diff --git a/.env.example b/.env.example index e5b05d155..86e065ad0 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ ROOMORAMA_SECRET_ATLEISURE=xxx ROOMORAMA_SECRET_POPLIDAYS=xxx ROOMORAMA_SECRET_CIIRUS=xxx ROOMORAMA_SECRET_SAW=xxx +ROOMORAMA_SECRET_RENTALS_UNITED=xxx ROLLBAR_ACCESS_TOKEN=not_used_in_development CONCIERGE_WEB_APP_SECRET=12345 SERVE_STATIC_ASSETS=true diff --git a/CHANGELOG.md b/CHANGELOG.md index 0514ff9e5..93f6e9d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ This file summarises the most important changes that went live on each release of Concierge. Please check the Wiki entry on the release process to understand how this file is formatted and how the process works. +## [0.12.0] - 2016-10-05 +### Added +- Rentals Untied sync, quote, book and cancel + +### Changed +- Abstract host fee calculation from suppliers to entity level +- Return 404 for attempts to quote a property not in records + ## [0.11.6] - 2016-10-03 ### Fixed - Proper order of table columns for sync_process/index page diff --git a/apps/api/config/environment_variables.yml b/apps/api/config/environment_variables.yml index a5dc050f4..a7140cc80 100644 --- a/apps/api/config/environment_variables.yml +++ b/apps/api/config/environment_variables.yml @@ -6,4 +6,5 @@ - ROOMORAMA_SECRET_WAYTOSTAY - ROOMORAMA_SECRET_CIIRUS - ROOMORAMA_SECRET_SAW +- ROOMORAMA_SECRET_RENTALS_UNITED - ZENDESK_NOTIFY_URL diff --git a/apps/api/config/initializers/validate_supplier_credentials.rb b/apps/api/config/initializers/validate_supplier_credentials.rb index 3b0d22fe1..d5dca784e 100644 --- a/apps/api/config/initializers/validate_supplier_credentials.rb +++ b/apps/api/config/initializers/validate_supplier_credentials.rb @@ -2,13 +2,14 @@ if enforce_on_envs.include?(Hanami.env) Concierge::Credentials.validate_credentials!({ - atleisure: %w(username password test_mode), - jtb: %w(id user password company url), - kigo: %w(subscription_key), - kigolegacy: %w(username password), - waytostay: %w(client_id client_secret url token_url), - ciirus: %w(url username password), - saw: %w(username password url), - poplidays: %w(url client_key passphrase) + atleisure: %w(username password test_mode), + jtb: %w(id user password company url), + kigo: %w(subscription_key), + kigolegacy: %w(username password), + waytostay: %w(client_id client_secret url token_url), + ciirus: %w(url username password), + saw: %w(username password url), + poplidays: %w(url client_key passphrase), + rentalsunited: %w(username password url) }) end diff --git a/apps/api/config/routes.rb b/apps/api/config/routes.rb index a854bd0d1..5b95deb46 100644 --- a/apps/api/config/routes.rb +++ b/apps/api/config/routes.rb @@ -1,28 +1,31 @@ -post '/atleisure/quote', to: 'at_leisure#quote' -post '/jtb/quote', to: 'j_t_b#quote' -post '/kigo/quote', to: 'kigo#quote' -post '/kigo/legacy/quote', to: 'kigo/legacy#quote' -post '/poplidays/quote', to: 'poplidays#quote' -post '/ciirus/quote', to: 'ciirus#quote' -post '/waytostay/quote', to: 'waytostay#quote' -post '/saw/quote', to: 's_a_w#quote' +post '/atleisure/quote', to: 'at_leisure#quote' +post '/jtb/quote', to: 'j_t_b#quote' +post '/kigo/quote', to: 'kigo#quote' +post '/kigo/legacy/quote', to: 'kigo/legacy#quote' +post '/poplidays/quote', to: 'poplidays#quote' +post '/ciirus/quote', to: 'ciirus#quote' +post '/waytostay/quote', to: 'waytostay#quote' +post '/saw/quote', to: 's_a_w#quote' +post '/rentalsunited/quote', to: 'rentals_united#quote' -post '/jtb/booking', to: 'j_t_b#booking' -post '/atleisure/booking', to: 'at_leisure#booking' -post '/waytostay/booking', to: 'waytostay#booking' -post '/ciirus/booking', to: 'ciirus#booking' -post '/kigo/booking', to: 'kigo#booking' -post '/kigo/legacy/booking', to: 'kigo/legacy#booking' -post '/saw/booking', to: 's_a_w#booking' -post '/poplidays/booking', to: 'poplidays#booking' +post '/jtb/booking', to: 'j_t_b#booking' +post '/atleisure/booking', to: 'at_leisure#booking' +post '/waytostay/booking', to: 'waytostay#booking' +post '/ciirus/booking', to: 'ciirus#booking' +post '/kigo/booking', to: 'kigo#booking' +post '/kigo/legacy/booking', to: 'kigo/legacy#booking' +post '/saw/booking', to: 's_a_w#booking' +post '/poplidays/booking', to: 'poplidays#booking' +post '/rentalsunited/booking', to: 'rentals_united#booking' -post '/waytostay/cancel', to: 'waytostay#cancel' -post '/ciirus/cancel', to: 'ciirus#cancel' -post '/saw/cancel', to: 's_a_w#cancel' -post '/kigo/cancel', to: 'kigo#cancel' -post '/kigo/legacy/cancel', to: 'kigo/legacy#cancel' -post '/poplidays/cancel', to: 'poplidays#cancel' -post '/atleisure/cancel', to: 'at_leisure#cancel' +post '/waytostay/cancel', to: 'waytostay#cancel' +post '/ciirus/cancel', to: 'ciirus#cancel' +post '/saw/cancel', to: 's_a_w#cancel' +post '/kigo/cancel', to: 'kigo#cancel' +post '/kigo/legacy/cancel', to: 'kigo/legacy#cancel' +post '/poplidays/cancel', to: 'poplidays#cancel' +post '/atleisure/cancel', to: 'at_leisure#cancel' +post '/rentalsunited/cancel', to: 'rentals_united#cancel' post '/checkout', to: 'static#checkout' get '/kigo/image/:property_id/:image_id', to: 'kigo#image' diff --git a/apps/api/controllers/quote.rb b/apps/api/controllers/quote.rb index ed1a4a2b5..839947241 100644 --- a/apps/api/controllers/quote.rb +++ b/apps/api/controllers/quote.rb @@ -46,6 +46,9 @@ def self.included(base) def call(params) if params.valid? + return status 500, error_response("No supplier record in database.") unless supplier + return status 404, error_response("Property not found") unless property_exists?(params[:property_id]) + quotation_result = quote_price(params) if quotation_result.success? @@ -54,16 +57,16 @@ def call(params) else announce_error(quotation_result) error_message = quotation_result.error.data || { quote: GENERIC_ERROR } - status 503, invalid_request(error_message) + status 503, error_response(error_message) end else - status 422, invalid_request(params.error_messages) + status 422, error_response(params.error_messages) end end private - def invalid_request(errors) + def error_response(errors) response = { status: "error" }.merge!(errors: errors) json_encode(response) end @@ -78,6 +81,15 @@ def announce_error(result) }) end + def property_exists?(id) + ! PropertyRepository.identified_by(id). + from_supplier(supplier).first.nil? + end + + def supplier + @supplier ||= SupplierRepository.named supplier_name + end + # Get the quote result from client implementations. # # The +params+ argument given is an instance of +API::Controllers::Params::Quote+. @@ -95,8 +107,9 @@ def quote_price(params) raise NotImplementedError end - # This is used when reporting errors from the supplier. - # Should return a string + # Should return a string. + # This is used when reporting error and + # searching for property def supplier_name raise NotImplementedError end diff --git a/apps/api/controllers/rentals_united/booking.rb b/apps/api/controllers/rentals_united/booking.rb new file mode 100644 index 000000000..c6648bf7b --- /dev/null +++ b/apps/api/controllers/rentals_united/booking.rb @@ -0,0 +1,31 @@ +require_relative "../booking" +require_relative "../params/booking" + +module API::Controllers::RentalsUnited + + # +API::Controllers::RentalsUnited::Booking+ + # + # Performs create booking for properties from RentalsUnited. + class Booking + include API::Controllers::Booking + + params API::Controllers::Params::Booking + + # Make property booking request + # + # Usage + # + # It returns a +Reservation+ object in both success and fail cases: + # + # API::Controllers::RentalsUnited::Booking.create_booking(selected_params) + # => Reservation(..) + def create_booking(params) + credentials = Concierge::Credentials.for(supplier_name) + RentalsUnited::Client.new(credentials).book(params) + end + + def supplier_name + RentalsUnited::Client::SUPPLIER_NAME + end + end +end diff --git a/apps/api/controllers/rentals_united/cancel.rb b/apps/api/controllers/rentals_united/cancel.rb new file mode 100644 index 000000000..fb3d0fa61 --- /dev/null +++ b/apps/api/controllers/rentals_united/cancel.rb @@ -0,0 +1,21 @@ +require_relative "../cancel" + +module API::Controllers::RentalsUnited + # +API::Controllers::RentalsUnited::Cancel+ + # + # Cancels reservation from RentalsUnited. + class Cancel + include API::Controllers::Cancel + + params API::Controllers::Params::Cancel + + def cancel_reservation(params) + credentials = Concierge::Credentials.for(supplier_name) + RentalsUnited::Client.new(credentials).cancel(params) + end + + def supplier_name + RentalsUnited::Client::SUPPLIER_NAME + end + end +end diff --git a/apps/api/controllers/rentals_united/quote.rb b/apps/api/controllers/rentals_united/quote.rb new file mode 100644 index 000000000..59ec17d70 --- /dev/null +++ b/apps/api/controllers/rentals_united/quote.rb @@ -0,0 +1,25 @@ +require_relative "../quote" + +module API::Controllers::RentalsUnited + # API::Controllers::RentalsUnited::Quote + # + # Performs booking quotations for properties from RentalsUnited. + class Quote + include API::Controllers::Quote + + params API::Controllers::Params::Quote + + # Make price (property rate) request + # + # Returns a +Result+ wrapping a +Quotation+ when operation succeeds + # Returns a +Result+ with +Result::Error+ when operation fails + def quote_price(params) + credentials = Concierge::Credentials.for(supplier_name) + RentalsUnited::Client.new(credentials).quote(params) + end + + def supplier_name + RentalsUnited::Client::SUPPLIER_NAME + end + end +end diff --git a/apps/api/middlewares/authentication.rb b/apps/api/middlewares/authentication.rb index eec19c577..91060acf9 100644 --- a/apps/api/middlewares/authentication.rb +++ b/apps/api/middlewares/authentication.rb @@ -46,14 +46,15 @@ class Authentication # secrets.for(request_path) # => X32842I class Secrets APP_SECRETS = { - "/jtb" => ENV["ROOMORAMA_SECRET_JTB"], - "/kigo/legacy" => ENV["ROOMORAMA_SECRET_KIGO_LEGACY"], - "/kigo" => ENV["ROOMORAMA_SECRET_KIGO"], - "/atleisure" => ENV["ROOMORAMA_SECRET_ATLEISURE"], - "/poplidays" => ENV["ROOMORAMA_SECRET_POPLIDAYS"], - "/waytostay" => ENV["ROOMORAMA_SECRET_WAYTOSTAY"], - "/ciirus" => ENV["ROOMORAMA_SECRET_CIIRUS"], - "/saw" => ENV["ROOMORAMA_SECRET_SAW"] + "/jtb" => ENV["ROOMORAMA_SECRET_JTB"], + "/kigo/legacy" => ENV["ROOMORAMA_SECRET_KIGO_LEGACY"], + "/kigo" => ENV["ROOMORAMA_SECRET_KIGO"], + "/atleisure" => ENV["ROOMORAMA_SECRET_ATLEISURE"], + "/poplidays" => ENV["ROOMORAMA_SECRET_POPLIDAYS"], + "/waytostay" => ENV["ROOMORAMA_SECRET_WAYTOSTAY"], + "/ciirus" => ENV["ROOMORAMA_SECRET_CIIRUS"], + "/saw" => ENV["ROOMORAMA_SECRET_SAW"], + "/rentalsunited" => ENV["ROOMORAMA_SECRET_RENTALS_UNITED"] } attr_reader :mapping diff --git a/apps/workers/suppliers/rentals_united/metadata.rb b/apps/workers/suppliers/rentals_united/metadata.rb new file mode 100644 index 000000000..e69094061 --- /dev/null +++ b/apps/workers/suppliers/rentals_united/metadata.rb @@ -0,0 +1,252 @@ +module Workers::Suppliers::RentalsUnited + # +Workers::Suppliers::RentalsUnited+ + # + # Performs property & calendar synchronisation with supplier. + # + # Decision to merge two workers in one was made because of the prices + # issue when we needed to fetch prices with the same API calls in both + # metadata and calendar sync workers. + # + # See more in corresponding PR discussion: + # https://github.com/roomorama/concierge/pull/309#pullrequestreview-682041 + class Metadata + attr_reader :property_sync, :calendar_sync, :host + + # Prevent from publishing property results containing error codes below. + IGNORABLE_ERROR_CODES = [ + :empty_seasons, + :attempt_to_build_archived_property, + :attempt_to_build_not_active_property, + :security_deposit_not_supported, + :property_type_not_supported + ] + + def initialize(host) + @host = host + @property_sync = Workers::PropertySynchronisation.new(host) + @calendar_sync = Workers::CalendarSynchronisation.new(host) + end + + def perform + result = property_sync.new_context { fetch_owner(host.identifier) } + return unless result.success? + owner = result.value + + result = fetch_properties_collection_for_owner(owner.id) + return unless result.success? + properties_collection = result.value + + result = fetch_locations(properties_collection.location_ids) + return unless result.success? + locations = result.value + + result = fetch_location_currencies + return unless result.success? + currencies = result.value + + properties_collection.each_entry do |property_id, location_id| + location = locations.find { |l| l.id == location_id } + + unless location + message = "Failed to find location with id `#{location_id}`" + announce_context_error(message, Result.error(:location_not_found)) + next + end + + location.currency = currencies[location.id] + + unless location.currency + message = "Failed to find currency for location with id `#{location_id}`" + announce_context_error(message, Result.error(:currency_not_found)) + next + end + + property_result = property_sync.new_context { fetch_property(property_id) } + next unless property_result.success? + property = property_result.value + + seasons_result = fetch_seasons(property_id) + next unless seasons_result.success? + seasons = seasons_result.value + + result = build_roomorama_property(property, location, owner, seasons) + + unless skip?(result, property) + property_sync.start(property_id) { result } if result.success? + end + + if synced_property?(property_id) + sync_calendar(property_id, seasons) + end + end + + property_sync.finish! + end + + private + # Performs calendar (availabilities + seasons) synchronisation for + # given property_id. + def sync_calendar(property_id, seasons) + calendar_sync.start(property_id) do + result = fetch_availabilities(property_id) + next result unless result.success? + availabilities = result.value + + mapper = ::RentalsUnited::Mappers::Calendar.new( + property_id, + seasons, + availabilities + ) + mapper.build_calendar + end + + calendar_sync.finish! + end + + # Checks whether property exists in database or not. + # Even if property was not synced in current synchronisation process, it's + # possible that it was synced before. + # + # Performs query every time without caching identifiers. + # + # We can't cache identifiers because calendar sync should be started right + # after each property sync and not when sync of all properties finished. + # + # (Otherwise we'll lose some data fetched inside the first sync process and + # then we'll need to cache all data in memory so then we can reuse it) + # + # We can switch to the different strategy if memory usage will not be high + # and we'll need to save database queries. + def synced_property?(property_id) + PropertyRepository.from_host(host).identified_by(property_id).count > 0 + end + + def importer + @properties ||= ::RentalsUnited::Importer.new(credentials) + end + + def credentials + @credentials ||= Concierge::Credentials.for( + ::RentalsUnited::Client::SUPPLIER_NAME + ) + end + + def build_roomorama_property(property, location, owner, seasons) + mapper = ::RentalsUnited::Mappers::RoomoramaProperty.new( + property, + location, + owner, + seasons + ) + mapper.build_roomorama_property + end + + def fetch_owner(owner_id) + sync_failed do + announce_error("Failed to fetch owner with owner_id `#{owner_id}`") do + importer.fetch_owner(owner_id) + end + end + end + + def fetch_properties_collection_for_owner(owner_id) + sync_failed do + announce_error("Failed to fetch property ids collection for owner `#{owner_id}`") do + importer.fetch_properties_collection_for_owner(owner_id) + end + end + end + + def fetch_locations(location_ids) + sync_failed do + announce_error("Failed to fetch locations with ids `#{location_ids}`") do + importer.fetch_locations(location_ids) + end + end + end + + def fetch_location_currencies + sync_failed do + announce_error("Failed to fetch locations-currencies mapping") do + importer.fetch_location_currencies + end + end + end + + def fetch_property(property_id) + sync_failed do + message = "Failed to fetch property with property_id `#{property_id}`" + announce_error(message) do + importer.fetch_property(property_id) + end + end + end + + def fetch_seasons(property_id) + sync_failed do + report_error("Failed to fetch seasons for property `#{property_id}`") do + importer.fetch_seasons(property_id) + end + end + end + + def fetch_availabilities(property_id) + report_error("Failed to fetch availabilities for property `#{property_id}`") do + importer.fetch_availabilities(property_id) + end + end + + def skip?(result, property) + if !result.success? && IGNORABLE_ERROR_CODES.include?(result.error.code) + property_sync.skip_property(property.id, result.error.code) + return true + end + return false + end + + def announce_error(message) + yield.tap do |result| + announce_context_error(message, result) unless result.success? + end + end + + def sync_failed + yield.tap do |result| + property_sync.failed! unless result.success? + end + end + + def report_error(message) + yield.tap do |result| + augment_context_error(message) unless result.success? + end + end + + def augment_context_error(message) + message = { + label: 'Synchronisation Failure', + message: message, + backtrace: caller + } + context = Concierge::Context::Message.new(message) + Concierge.context.augment(context) + end + + def announce_context_error(message, result) + augment_context_error(message) + + Concierge::Announcer.trigger(Concierge::Errors::EXTERNAL_ERROR, { + operation: 'sync', + supplier: RentalsUnited::Client::SUPPLIER_NAME, + code: result.error.code, + context: Concierge.context.to_h, + happened_at: Time.now + }) + end + end +end + +Concierge::Announcer.on("metadata.RentalsUnited") do |host, args| + Workers::Suppliers::RentalsUnited::Metadata.new(host).perform + Result.new({}) +end diff --git a/config/credentials/production.yml b/config/credentials/production.yml index 4eb61248c..f17371000 100644 --- a/config/credentials/production.yml +++ b/config/credentials/production.yml @@ -44,3 +44,8 @@ poplidays: url: <%= ENV["POPLIDAYS_URL"] %> client_key: <%= ENV["POPLIDAYS_CLIENT_KEY"] %> passphrase: <%= ENV["POPLIDAYS_PASSPHRASE"] %> + +rentalsunited: + username: <%= ENV["RENTALS_UNITED_USERNAME"] %> + password: <%= ENV["RENTALS_UNITED_PASSWORD"] %> + url: <%= ENV["RENTALS_UNITED_URL"] %> diff --git a/config/credentials/staging.yml b/config/credentials/staging.yml index e0bcaa086..198dcc032 100644 --- a/config/credentials/staging.yml +++ b/config/credentials/staging.yml @@ -44,3 +44,8 @@ poplidays: url: <%= ENV["POPLIDAYS_URL"] %> client_key: <%= ENV["POPLIDAYS_CLIENT_KEY"] %> passphrase: <%= ENV["POPLIDAYS_PASSPHRASE"] %> + +rentalsunited: + username: <%= ENV["RENTALS_UNITED_USERNAME"] %> + password: <%= ENV["RENTALS_UNITED_PASSWORD"] %> + url: <%= ENV["RENTALS_UNITED_URL"] %> diff --git a/config/credentials/test.yml b/config/credentials/test.yml index fa340198a..884cde75a 100644 --- a/config/credentials/test.yml +++ b/config/credentials/test.yml @@ -52,3 +52,8 @@ poplidays: url: 'www.example.org' client_key: "123" passphrase: "123" + +rentalsunited: + username: "roomorama-user" + password: "roomorama-pass" + url: "http://www.example.org" diff --git a/config/suppliers.yml b/config/suppliers.yml index e52aac053..bc190cbc9 100644 --- a/config/suppliers.yml +++ b/config/suppliers.yml @@ -57,3 +57,8 @@ Poplidays: every: "1d" availabilities: every: "5h" + +RentalsUnited: + workers: + metadata: + every: "1d" diff --git a/lib/concierge/entities/quotation.rb b/lib/concierge/entities/quotation.rb index 42247194c..302de13ca 100644 --- a/lib/concierge/entities/quotation.rb +++ b/lib/concierge/entities/quotation.rb @@ -13,30 +13,41 @@ # +available+: whether or not the property is available for the given dates # +total+: the quoted price for the booking # +currency+: the currency used for the quotation -# +host_fee_percentage+: the host fee percent, which included in quoted price # # The quotation is only successful if the +errors+ attribute is empty. class Quotation include Hanami::Entity include Hanami::Validations - attribute :property_id, type: String - attribute :unit_id, type: String - attribute :check_in, type: String - attribute :check_out, type: String - attribute :guests, type: Integer - attribute :available, type: Boolean - attribute :total, type: Float - attribute :currency, type: String - attribute :host_fee_percentage, type: Float + attribute :property_id, type: String + attribute :unit_id, type: String + attribute :check_in, type: String + attribute :check_out, type: String + attribute :guests, type: Integer + attribute :available, type: Boolean + attribute :total, type: Float + attribute :currency, type: String + #validates :net_rate, presence: true + + # The fee, already included in quoted total price def host_fee (total - net_rate).round(2) end + # The quotation without host fee def net_rate coefficient = 1 + (host_fee_percentage.to_f / 100) (total / coefficient).round(2) end + # The fee percentage, already included in quoted total price + def host_fee_percentage + @host_fee_percentage ||= HostRepository.find(property.host_id).fee_percentage + end + + def property + @property ||= PropertyRepository.identified_by(property_id).first + end + end diff --git a/lib/concierge/suppliers/atleisure/price.rb b/lib/concierge/suppliers/atleisure/price.rb index 42c795237..f07b1f5a5 100644 --- a/lib/concierge/suppliers/atleisure/price.rb +++ b/lib/concierge/suppliers/atleisure/price.rb @@ -38,9 +38,6 @@ def initialize(credentials) # Calls the CheckAvailabilityV1 method from the AtLeisure JSON-RPC interface. # Returns a +Result+ object. def quote(params) - host = fetch_host - return host_not_found unless host - stay_details = { "HouseCode" => params[:property_id], "ArrivalDate" => params[:check_in].to_s, @@ -52,7 +49,7 @@ def quote(params) result = client.invoke("CheckAvailabilityV1", stay_details) if result.success? - parse_quote_response(params, result.value, host.fee_percentage) + parse_quote_response(params, result.value) else result end @@ -60,19 +57,8 @@ def quote(params) private - # Get the first host under AtLeisure, because there - # should only be one host - def fetch_host - supplier = SupplierRepository.named(AtLeisure::Client::SUPPLIER_NAME) - HostRepository.from_supplier(supplier).first - end - - def host_not_found - Result.error(:host_not_found) - end - - def parse_quote_response(params, response, host_fee_percentage) - quotation = build_quotation(params, host_fee_percentage) + def parse_quote_response(params, response) + quotation = build_quotation(params) if response["OnRequest"] == "Yes" no_instant_confirmation @@ -100,14 +86,13 @@ def parse_quote_response(params, response, host_fee_percentage) end end - def build_quotation(params, host_fee_percentage) + def build_quotation(params) Quotation.new( property_id: params[:property_id], check_in: params[:check_in].to_s, check_out: params[:check_out].to_s, guests: params[:guests], currency: CURRENCY, - host_fee_percentage: host_fee_percentage, ) end diff --git a/lib/concierge/suppliers/ciirus/client.rb b/lib/concierge/suppliers/ciirus/client.rb index b9392e05e..35f4a956f 100644 --- a/lib/concierge/suppliers/ciirus/client.rb +++ b/lib/concierge/suppliers/ciirus/client.rb @@ -25,7 +25,7 @@ def initialize(credentials) # Ciirus, a generic error message is sent back to the caller, and the failure # is logged. def quote(params) - Ciirus::Price.new(credentials).quote(params) + Ciirus::Commands::QuoteFetcher.new(credentials).call(params) end # Returns a +Result+ wrapping +Reservation+ in success case. @@ -44,4 +44,4 @@ def cancel(params) Ciirus::Commands::Cancel.new(credentials).call(params[:reference_number]) end end -end \ No newline at end of file +end diff --git a/lib/concierge/suppliers/ciirus/price.rb b/lib/concierge/suppliers/ciirus/price.rb deleted file mode 100644 index 2bc96c4fa..000000000 --- a/lib/concierge/suppliers/ciirus/price.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Ciirus - - # +Ciirus::Price+ - # - # This class is responsible for wrapping the logic related to making a price quotation - # to Ciirus, parsing the response, and building the +Quotation+ object with the data - # returned from their API. - # - # Usage - # - # result = Ciirus::Price.new(credentials).quote(stay_params) - # if result.success? - # process_quotation(result.value) - # else - # handle_error(result.error) - # end - # - # The +quote+ method returns a +Result+ object that, when successful, encapsulates the - # resulting +Quotation+ object. - # Actually the main logic of building the +Quotation+ object is in +QuoteFetcher+ class, - # while +Price+ responsible for filling +host_fee_percentage+ field. - class Price - attr_reader :credentials - - def initialize(credentials) - @credentials = credentials - end - - def quote(params) - property = fetch_property(params[:property_id]) - return property_not_found unless property - host = fetch_host(property.host_id) - return host_not_found unless host - - quotation = Ciirus::Commands::QuoteFetcher.new(credentials).call(params) - return quotation unless quotation.success? - - quotation.value.host_fee_percentage = host.fee_percentage - quotation - end - - def property_not_found - Result.error(:property_not_found) - end - - def host_not_found - Result.error(:host_not_found) - end - - def fetch_host(id) - HostRepository.find(id) - end - - def fetch_property(id) - PropertyRepository.identified_by(id).first - end - end -end \ No newline at end of file diff --git a/lib/concierge/suppliers/kigo/response_parser.rb b/lib/concierge/suppliers/kigo/response_parser.rb index 5beea84bd..878d1f4d8 100644 --- a/lib/concierge/suppliers/kigo/response_parser.rb +++ b/lib/concierge/suppliers/kigo/response_parser.rb @@ -28,7 +28,6 @@ def initialize(params) # in case the response is successful. Possible errors that could # happen in this step are: # - # +property_not_found+: the param's +property_id+ doesn't persist in our database. # +invalid_json_representation+: the response sent back is not a valid JSON. # +quote_call_failed+: the response status is not +E_OK+. # +unrecognised_response+: the response was successful, but the format cannot @@ -57,11 +56,8 @@ def compute_pricing(response) end end - return property_not_found unless property - return host_not_found unless host quotation.available = true - quotation.host_fee_percentage = host.fee_percentage quotation.currency = currency quotation.total = total.to_f @@ -175,22 +171,6 @@ def unrecognised_response Result.error(:unrecognised_response) end - def property_not_found - Result.error(:property_not_found) - end - - def host_not_found - Result.error(:host_not_found) - end - - def host - @host ||= HostRepository.find(property.host_id) - end - - def property - @property ||= PropertyRepository.identified_by(params[:property_id]).first - end - def non_successful_result_code message = "The `API_RESULT_CODE` obtained was not equal to `E_OK`. Check Kigo's " + "API documentation for an explanation for the `API_RESULT_CODE` returned." diff --git a/lib/concierge/suppliers/poplidays/mappers/quote.rb b/lib/concierge/suppliers/poplidays/mappers/quote.rb index 19d6aaff4..3df6560ec 100644 --- a/lib/concierge/suppliers/poplidays/mappers/quote.rb +++ b/lib/concierge/suppliers/poplidays/mappers/quote.rb @@ -17,16 +17,14 @@ class Quote # * +mandatory_services+ [Float] mandatory services price # * +quote+ [Result] result which contains response from Poplidays booking/easy method # in "EVALUATION" mode - # * +host_fee_percentage+ [Float] # Returns a +Result+ wrapping +Roomorama::Quotation+ - def build(params, mandatory_services, quote, host_fee_percentage) + def build(params, mandatory_services, quote) quotation = ::Quotation.new( property_id: params[:property_id], check_in: params[:check_in].to_s, check_out: params[:check_out].to_s, guests: params[:guests], available: available?(quote), - host_fee_percentage: host_fee_percentage ) if quotation.available return Result.error(:unexpected_quote) unless quote.value['value'] @@ -48,4 +46,4 @@ def calc_total(mandaroty_services, quote) end end end -end \ No newline at end of file +end diff --git a/lib/concierge/suppliers/poplidays/price.rb b/lib/concierge/suppliers/poplidays/price.rb index 876b5dc2f..920b8385c 100644 --- a/lib/concierge/suppliers/poplidays/price.rb +++ b/lib/concierge/suppliers/poplidays/price.rb @@ -44,16 +44,13 @@ def initialize(credentials) # for when calculating the subtotal. For that purpose, an API call to # the property details endpoint is made and that value is extracted. def quote(params) - host = fetch_host - return host_not_found unless host - mandatory_services = retrieve_mandatory_services(params[:property_id]) return mandatory_services unless mandatory_services.success? quote = retrieve_quote(params) return quote if unknown_errors?(quote) - mapper.build(params, mandatory_services.value, quote, host.fee_percentage) + mapper.build(params, mandatory_services.value, quote) end private @@ -66,16 +63,6 @@ def unknown_errors?(quote) !quote.success? && ![:http_status_400, :http_status_409].include?(quote.error.code) end - # Get the first Poplidays host, because there should only be one host - def fetch_host - supplier = SupplierRepository.named(Poplidays::Client::SUPPLIER_NAME) - HostRepository.from_supplier(supplier).first - end - - def host_not_found - Result.error(:host_not_found) - end - def retrieve_quote(params) fetcher = Poplidays::Commands::QuoteFetcher.new(credentials) fetcher.call(params) diff --git a/lib/concierge/suppliers/rentals_united/client.rb b/lib/concierge/suppliers/rentals_united/client.rb new file mode 100644 index 000000000..ba37c7642 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/client.rb @@ -0,0 +1,122 @@ +module RentalsUnited + # +RentalsUnited::Client+ + class Client + SUPPLIER_NAME = "RentalsUnited" + + attr_reader :credentials + + def initialize(credentials) + @credentials = credentials + end + + # Quote RentalsUnited properties prices + # + # If an error happens in any step in the process of getting a response back + # from RentalsUnited, a result object with error is returned + # + # Arguments + # + # * +quotation_params+ [Concierge::SafeAccessHash] stay parameters + # + # Stay parameters are defined by the set of attributes from + # +API::Controllers::Params::MultiUnitQuote+ params object. + # + # +quotation_params+ object includes: + # + # * +property_id+ + # * +check_in+ + # * +check_out+ + # * +guests+ + # + # Usage + # + # comamnd = RentalsUnited::Client.new(credentials) + # result = command.quote(params) + # + # if result.success? + # # ... + # end + # + # Returns a +Result+ wrapping a +Quotation+ when operation succeeds + # Returns a +Result+ with +Result::Error+ when operation fails + def quote(quotation_params) + host = find_host + return host_not_found unless host + + property = find_property(quotation_params[:property_id]) + return property_not_found unless property + + command = RentalsUnited::Commands::PriceFetcher.new( + credentials, + quotation_params + ) + result = command.call + return result unless result.success? + price = result.value + + mapper = RentalsUnited::Mappers::Quotation.new( + price, + property.data.get("currency"), + quotation_params + ) + Result.new(mapper.build_quotation) + end + + # RentalsUnited properties booking. + # + # If an error happens in any step in the process of getting a response back + # from RentalsUnited, a result object with error is returned + # + # Usage + # + # client = RentalsUnited::Client.new(credentials) + # result = client.book(reservation_params) + # + # Returns a +Result+ wrapping a +Reservation+ when operation succeeds + # Returns a +Result+ with +Result::Error+ when operation fails + def book(reservation_params) + command = RentalsUnited::Commands::Booking.new( + credentials, + reservation_params + ) + command.call + end + + # Cancels a reservation by given reference_number + # + # If an error happens in any step in the process of getting a response back + # from RentalsUnited, a result object with error is returned + # + # client = RentalsUnited::Client.new(credentials) + # result = client.cancel(params) + # + # Returns a +Result+ wrapping a +String+ with reference_number number when + # operation succeeds + # Returns a +Result+ with +Result::Error+ when operation fails + def cancel(params) + command = RentalsUnited::Commands::Cancel.new( + credentials, + params[:reference_number] + ) + command.call + end + + private + def find_host + supplier = SupplierRepository.named(SUPPLIER_NAME) + HostRepository.from_supplier(supplier).first + end + + def find_property(property_id) + PropertyRepository.identified_by(property_id).first + end + + def host_not_found + Result.error(:host_not_found) + end + + def property_not_found + Result.error(:property_not_found) + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/availabilities_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/availabilities_fetcher.rb new file mode 100644 index 000000000..d95f49624 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/availabilities_fetcher.rb @@ -0,0 +1,79 @@ +require_relative 'base_fetcher' + +module RentalsUnited + module Commands + # +RentalsUnited::Commands::AvailabilitiesFetcher+ + # + # This class is responsible for wrapping the logic related to fetching + # property availabilities from RentalsUnited + class AvailabilitiesFetcher < BaseFetcher + attr_reader :property_id + + ROOT_TAG = "Pull_ListPropertyAvailabilityCalendar_RS" + YEARS_COUNT_TO_FETCH = 1 + + # Initialize +AvailabilitiesFetcher+ command. + # + # Arguments + # + # * +credentials+ + # * +property_id+ [String] + # + # Usage: + # + # RentalsUnited::Commands::AvailabilitiesFetcher.new( + # credentials, + # property_id + # ) + def initialize(credentials, property_id) + super(credentials) + + @property_id = property_id + end + + # Retrieves property availabilities. + # + # Returns a +Result+ wrapping +Array+ of +Entities::Availability+ + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_availabilities + payload = payload_builder.build_availabilities_fetch_payload( + property_id, + date_from, + date_to + ) + result = http.post(credentials.url, payload, headers) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + Result.new(build_availabilities(result_hash)) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def date_from + Time.now.strftime("%Y-%m-%d") + end + + def date_to + current = Time.now + + year = current.year + YEARS_COUNT_TO_FETCH + date = Time.new(year, current.month, current.day) + date.strftime("%Y-%m-%d") + end + + def build_availabilities(hash) + days = Array(hash.get("#{ROOT_TAG}.PropertyCalendar.CalDay")) + days.map do |day| + mapper = Mappers::Availability.new(day) + mapper.build_availability + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/base_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/base_fetcher.rb new file mode 100644 index 000000000..619f30bd4 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/base_fetcher.rb @@ -0,0 +1,103 @@ +module RentalsUnited + module Commands + class BaseFetcher + attr_reader :credentials + + # Some statuses from RentalsUnited API are not actually errors, so this + # constant is a list of whitelisted codes, which we consider as normal + # behaviour. + # + # Whitelisted RentalsUnited API statuses: + # 0 - Success + # 1 - Property is not available for a given dates + # + # Some status codes are ambiguous: they considered as valid for one + # endpoint/command and not valid for other. Specific cases like this are + # handled in particular command classes. + + VALID_RU_STATUS_CODES = ["0", "1"] + + def initialize(credentials) + @credentials = credentials + end + + def payload_builder + @payload_builder ||= RentalsUnited::PayloadBuilder.new(credentials) + end + + def response_parser + @response_parser ||= RentalsUnited::ResponseParser.new + end + + # All API calls performed to the same base URL which determined in + # the instance of http client. + def api_call(payload) + http.post("", payload, headers) + end + + def get_status(hash, root_tag_name) + hash.get("#{root_tag_name}.Status") + end + + def get_status_code(status) + status.attributes["ID"] + end + + def get_status_description(code) + RentalsUnited::Dictionaries::Statuses.find(code) + end + + def valid_status?(hash, root_tag_name) + status = get_status(hash, root_tag_name) + + return false unless status + + VALID_RU_STATUS_CODES.include?(get_status_code(status)) + end + + def error_result(hash, root_tag_name) + status = get_status(hash, root_tag_name) + + if status + code = get_status_code(status) + description = get_status_description(code) + + augment_with_error(code, description, caller) + Result.error(code) + else + code = :unrecognised_response + unrecognised_response_event(caller) + end + Result.error(code) + end + + def augment_with_error(code, description, backtrace) + message = "Response indicating the Status with ID `#{code}`, and description `#{description}`" + mismatch(message, backtrace) + end + + def mismatch(message, backtrace) + response_mismatch = Concierge::Context::ResponseMismatch.new( + message: message, + backtrace: backtrace + ) + + Concierge.context.augment(response_mismatch) + end + + def unrecognised_response_event(backtrace) + message = "Error response could not be recognised (no `Status` tag in the response)" + mismatch(message, backtrace) + end + + private + def http + @http_client ||= Concierge::HTTPClient.new(credentials.url, timeout: 600) + end + + def headers + { "Content-Type" => "application/xml" } + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/booking.rb b/lib/concierge/suppliers/rentals_united/commands/booking.rb new file mode 100644 index 000000000..56546ca52 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/booking.rb @@ -0,0 +1,103 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::Booking+ + # + # This class is responsible for wrapping the logic related to making a + # reservation to RentalsUnited, parsing the response, and building the + # +Reservation+ object with the data returned from their API. + # + # Usage + # + # command = RentalsUnited::Commands::Booking.new(credentials, params) + # result = command.call + # + # if result.success? + # process_reservation(result.value) + # else + # handle_error(result.error) + # end + class Booking < BaseFetcher + attr_reader :reservation_params + + ROOT_TAG = "Push_PutConfirmedReservationMulti_RS" + + # Specifically this command consider these status codes like "error" + INVALID_PRICE_FETCH_STATUS_CODES = ["1"] + + # Initialize +Booking+ command. + # + # Arguments + # + # * +credentials+ + # * +reservation_params+ [Concierge::SafeAccessHash] stay parameters + # + # Stay parameters are defined by the set of attributes from + # +API::Controllers::Params::Booking+ params object. + # + # +reservation_params+ object includes: + # + # * +property_id+ + # * +check_in+ + # * +check_out+ + # * +guests+ + # * +subtotal+ + # * +customer+ + def initialize(credentials, reservation_params) + super(credentials) + @reservation_params = reservation_params + end + + # Calls the RentalsUnited API method using the HTTP client. + # + # Returns a +Result+ wrapping a +Reservation+ when operation succeeds + # Returns a +Result+ with +Result::Error+ when operation fails + def call + payload = build_payload + result = http.post(credentials.url, payload, headers) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + status = get_status(result_hash, ROOT_TAG) + code = get_status_code(status) + + if INVALID_PRICE_FETCH_STATUS_CODES.include?(code) + error_result(result_hash, ROOT_TAG) + else + Result.new(build_reservation(result_hash)) + end + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def build_payload + payload_builder.build_booking_payload( + property_id: reservation_params.get("property_id"), + check_in: reservation_params.get("check_in"), + check_out: reservation_params.get("check_out"), + num_guests: reservation_params.get("guests"), + total: reservation_params.get("subtotal"), + user: { + first_name: reservation_params.get("customer.first_name"), + last_name: reservation_params.get("customer.last_name"), + email: reservation_params.get("customer.email"), + phone: reservation_params.get("customer.phone"), + address: reservation_params.get("customer.address"), + postal_code: reservation_params.get("customer.postal_code") + } + ) + end + + def build_reservation(result_hash) + reservation_code = result_hash.get("#{ROOT_TAG}.ReservationID") + + mapper = Mappers::Reservation.new(reservation_code, reservation_params) + mapper.build_reservation + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/cancel.rb b/lib/concierge/suppliers/rentals_united/commands/cancel.rb new file mode 100644 index 000000000..034b11ad4 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/cancel.rb @@ -0,0 +1,48 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::Cancel+ + # + # This class is responsible for wrapping the logic related to cancellations + # properties for RentalsUnited. + class Cancel < BaseFetcher + attr_reader :reference_number + + ROOT_TAG = "Push_CancelReservation_RS" + + # Initialize +Cancel+ command. + # + # Arguments + # + # * +credentials+ + # * +reference_number+ [String] id of reservation to cancel + def initialize(credentials, reference_number) + super(credentials) + @reference_number = reference_number + end + + # Cancels reservation by its id (reservation code) + # + # RentalsUnited API returns just simple success response, so by calling + # `valid_status()` we actually checking whether cancellation is + # successful or not. + # + # Returns a +Result+ wrapping a +reference_number+ of the cancelled + # reservation when operation succeeds. + # Returns a +Result+ with +Result::Error+ when operation fails + def call + payload = payload_builder.build_cancel_payload(reference_number) + result = http.post(credentials.url, payload, headers) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + Result.new(reference_number) + else + error_result(result_hash, ROOT_TAG) + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/locations_currencies_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/locations_currencies_fetcher.rb new file mode 100644 index 000000000..5f1118227 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/locations_currencies_fetcher.rb @@ -0,0 +1,49 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::LocationCurrenciesFetcher+ + # + # This class is responsible for wrapping the logic related to fetching + # location currencies from RentalsUnited + class LocationCurrenciesFetcher < BaseFetcher + ROOT_TAG = "Pull_ListCurrenciesWithCities_RS" + + # Retrieves locations - currencies mapping. + # + # Returns a +Result+ wrapping +Hash+ with location_id => currency pairs + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_location_currencies + payload = payload_builder.build_location_currencies_fetch_payload + result = api_call(payload) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + location_currencies = build_location_currencies(result_hash) + Result.new(location_currencies) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def build_location_currencies(result_hash) + location_currencies = {} + + currencies_hash = result_hash.get("#{ROOT_TAG}.Currencies.Currency") + currencies_hash.each do |currency_hash| + safe_hash = Concierge::SafeAccessHash.new(currency_hash) + location_ids = Array(safe_hash.get("Locations.LocationID")) + currency_code = safe_hash.get("@CurrencyCode") + + location_ids.each do |location_id| + location_currencies[location_id] = currency_code + end + end + + location_currencies + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/locations_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/locations_fetcher.rb new file mode 100644 index 000000000..ed7441b48 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/locations_fetcher.rb @@ -0,0 +1,67 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::LocationsFetcher+ + # + # This class is responsible for wrapping the logic related to fetching + # locations from RentalsUnited, parsing the response, and building + # +Result+ object. + class LocationsFetcher < BaseFetcher + attr_reader :location_ids + + ROOT_TAG = "Pull_ListLocations_RS" + + def initialize(credentials, location_ids) + super(credentials) + @location_ids = location_ids + end + + # Retrieves locations by location_ids + # + # Returns a +Result+ wrapping +Array+ of +Entities::Location+ objects + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_locations + payload = payload_builder.build_locations_fetch_payload + result = api_call(payload) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + raw_locations = build_raw_locations(result_hash) + + locations = location_ids.map do |id| + location = build_location(id, raw_locations) + + return Result.error(:unknown_location) unless location + + location + end + + Result.new(locations) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def build_raw_locations(hash) + locations = hash.get("Pull_ListLocations_RS.Locations.Location") + + Array(locations).map do |l| + { + id: l.attributes["LocationID"], + name: l.to_s, + type: l.attributes["LocationTypeID"].to_i, + parent_id: l.attributes["ParentLocationID"] + } + end + end + + def build_location(location_id, raw_locations) + mapper = Mappers::Location.new(location_id, raw_locations) + mapper.build_location + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/owner_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/owner_fetcher.rb new file mode 100644 index 000000000..0c7c3334a --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/owner_fetcher.rb @@ -0,0 +1,60 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::OwnerFetcher+ + # + # This class is responsible for wrapping the logic related to fetching + # owner from RentalsUnited + class OwnerFetcher < BaseFetcher + ROOT_TAG = "Pull_GetOwnerDetails_RS" + + attr_reader :owner_id + + # Initialize +OwnerFetcher+ command. + # + # Arguments + # + # * +credentials+ + # * +owner_id+ [String] + # + # Usage: + # + # RentalsUnited::Commands::OwnerFetcher.new(credentials, owner_id) + def initialize(credentials, owner_id) + super(credentials) + + @owner_id = owner_id + end + + # Retrieves owner. + # + # Returns a +Result+ wrapping +Entities::Owner+ + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_owner + payload = payload_builder.build_owner_fetch_payload(owner_id) + result = api_call(payload) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + Result.new(build_owner(result_hash)) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def build_owner(hash) + owner_hash = hash.get("#{ROOT_TAG}.Owner") + + if owner_hash + mapper = RentalsUnited::Mappers::Owner.new(owner_hash) + mapper.build_owner + else + Result.error(:owner_not_found) + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/price_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/price_fetcher.rb new file mode 100644 index 000000000..fa9ed022a --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/price_fetcher.rb @@ -0,0 +1,79 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::PriceFetcher+ + # + # This class is responsible for wrapping the logic related to making a + # price fetch to RentalsUnited, parsing the response, and building the + # +Entities::Price with the data returned from their API. + # + # Usage + # + # command = RentalsUnited::Commands::PriceFetcher.new( + # credentials, + # stay_params + # ) + # result = command.call + class PriceFetcher < BaseFetcher + attr_reader :stay_params + + ROOT_TAG = "Pull_GetPropertyAvbPrice_RS" + + # Initialize +PriceFetcher+ command. + # + # Arguments + # + # * +credentials+ + # * +stay_params+ [Concierge::SafeAccessHash] stay parameters + # + # Stay parameters are defined by the set of attributes from + # +API::Controllers::Params::MultiUnitQuote+ params object. + # + # +stay_params+ object includes: + # + # * +property_id+ + # * +check_in+ + # * +check_out+ + # * +guests+ + def initialize(credentials, stay_params) + super(credentials) + @stay_params = stay_params + end + + # Calls the RentalsUnited API method using the HTTP client. + # + # Returns a +Result+ wrapping a +Entities::Price+ when operation succeeds + # Returns a +Result+ with +Result::Error+ when operation fails + def call + payload = build_payload + result = http.post(credentials.url, payload, headers) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + Result.new(build_price(result_hash)) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def build_payload + payload_builder.build_price_fetch_payload( + property_id: stay_params[:property_id], + check_in: stay_params[:check_in], + check_out: stay_params[:check_out], + num_guests: stay_params[:guests] + ) + end + + def build_price(result_hash) + price = result_hash.get("#{ROOT_TAG}.PropertyPrices.PropertyPrice") + + mapper = Mappers::Price.new(price) + mapper.build_price + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/properties_collection_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/properties_collection_fetcher.rb new file mode 100644 index 000000000..cd94ac5d0 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/properties_collection_fetcher.rb @@ -0,0 +1,61 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::PropertiesCollectionFetcher+ + # + # This class is responsible for wrapping the logic related to fetching + # properties collection for a given owner from RentalsUnited + class PropertiesCollectionFetcher < BaseFetcher + ROOT_TAG = "Pull_ListOwnerProp_RS" + + attr_reader :owner_id + + # Initialize +PropertiesCollectionFetcher+ command. + # + # Arguments + # + # * +credentials+ + # * +owner_id+ [String] + # + # Usage: + # + # RentalsUnited::Commands::PropertiesCollectionFetcher.new( + # credentials, + # owner_id + # ) + def initialize(credentials, owner_id) + super(credentials) + + @owner_id = owner_id + end + + # Retrieves properties collection. + # + # Returns a +Result+ wrapping +Entities::PropertiesCollection+ + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_properties_collection_for_owner + payload = payload_builder.build_properties_collection_fetch_payload( + owner_id + ) + result = api_call(payload) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + Result.new(build_properties_collection(result_hash)) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def build_properties_collection(hash) + properties = Array(hash.get("#{ROOT_TAG}.Properties.Property")) + + mapper = RentalsUnited::Mappers::PropertiesCollection.new(properties) + mapper.build_properties_collection + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/property_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/property_fetcher.rb new file mode 100644 index 000000000..8f60ea8f0 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/property_fetcher.rb @@ -0,0 +1,61 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::PropertyFetcher+ + # + # This class is responsible for wrapping the logic related to fetching + # property from RentalsUnited, parsing the response, and building + # +Result+ object. + class PropertyFetcher < BaseFetcher + attr_reader :property_id + + ROOT_TAG = "Pull_ListSpecProp_RS" + + # Initialize +PropertyFetcher+ command. + # + # Arguments + # + # * +credentials+ + # * +property_id+ [String] + # + # Usage: + # + # RentalsUnited::Commands::PropertyFetcher.new( + # credentials, + # property_id + # ) + def initialize(credentials, property_id) + super(credentials) + + @property_id = property_id + end + + # Retrieves property + # + # Returns a +Result+ wrapping +Entities::Property+ object + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_property + payload = payload_builder.build_property_fetch_payload(property_id) + result = api_call(payload) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + Result.new(build_property(result_hash)) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def build_property(hash) + property = hash.get("#{ROOT_TAG}.Property") + return error_result(hash, ROOT_TAG) unless property + + mapper = RentalsUnited::Mappers::Property.new(property) + mapper.build_property + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/commands/seasons_fetcher.rb b/lib/concierge/suppliers/rentals_united/commands/seasons_fetcher.rb new file mode 100644 index 000000000..6cc83fea7 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/commands/seasons_fetcher.rb @@ -0,0 +1,77 @@ +module RentalsUnited + module Commands + # +RentalsUnited::Commands::SeasonsFetcher+ + # + # This class is responsible for wrapping the logic related to fetching + # property rate seasons from RentalsUnited + class SeasonsFetcher < BaseFetcher + attr_reader :property_id + + ROOT_TAG = "Pull_ListPropertyPrices_RS" + YEARS_COUNT_TO_FETCH = 1 + + # Initialize +SeasonsFetcher+ command. + # + # Arguments + # + # * +credentials+ + # * +property_id+ [String] + # + # Usage: + # + # RentalsUnited::Commands::SeasonsFetcher.new( + # credentials, + # property_id + # ) + def initialize(credentials, property_id) + super(credentials) + + @property_id = property_id + end + + # Retrieves property rate seasons. + # + # Returns a +Result+ wrapping +Array+ of +Entities::Season+ objects + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_seasons + payload = payload_builder.build_seasons_fetch_payload( + property_id, + date_from, + date_to + ) + result = http.post(credentials.url, payload, headers) + + return result unless result.success? + + result_hash = response_parser.to_hash(result.value.body) + + if valid_status?(result_hash, ROOT_TAG) + Result.new(build_seasons(result_hash)) + else + error_result(result_hash, ROOT_TAG) + end + end + + private + def date_from + Time.now.strftime("%Y-%m-%d") + end + + def date_to + current = Time.now + + year = current.year + YEARS_COUNT_TO_FETCH + date = Time.new(year, current.month, current.day) + date.strftime("%Y-%m-%d") + end + + def build_seasons(hash) + seasons = Array(hash.get("#{ROOT_TAG}.Prices.Season")) + seasons.map do |season| + mapper = Mappers::Season.new(season) + mapper.build_season + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/converters/country_code.rb b/lib/concierge/suppliers/rentals_united/converters/country_code.rb new file mode 100644 index 000000000..3e3e7425e --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/converters/country_code.rb @@ -0,0 +1,28 @@ +module RentalsUnited + module Converters + # +RentalsUnited::Converters::CountryCode+ + # + # This class acts as a facade between country codes converter library and + # our source code for abstracting library's entities and using only needed + # part of the library API. + class CountryCode + class << self + # Returns country code by its name + # + # Arguments + # * +name+ [String] name of the country + # + # Example + # + # CountryCode.code_by_name("Korea, Republic of") + # => "KR" + # + # Returns [String] country code + def code_by_name(name) + country = IsoCountryCodes.search_by_name(name).first + (country && country.alpha2).to_s + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/amenities.json b/lib/concierge/suppliers/rentals_united/dictionaries/amenities.json new file mode 100644 index 000000000..a655c84d7 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/amenities.json @@ -0,0 +1,473 @@ +{ + "enabled-amenities": [ + { "id": "2", "name": "Cookware & Kitchen Utensils", "roomorama_name": "kitchen" }, + { "id": "7", "name": "Bed Linen & Towels", "roomorama_name": "bed_linen_and_towels" }, + { "id": "11", "name": "Washing Machine", "roomorama_name": "laundry" }, + { "id": "19", "name": "Cable TV", "roomorama_name": "cabletv" }, + { "id": "23", "name": "DVD", "roomorama_name": "tv" }, + { "id": "74", "name": "TV", "roomorama_name": "tv" }, + { "id": "89", "name": "Balcony", "roomorama_name": "balcony" }, + { "id": "96", "name": "Small Balcony", "roomorama_name": "balcony" }, + { "id": "101", "name": "Kitchen", "roomorama_name": "kitchen" }, + { "id": "134", "name": "washing machine with drier", "roomorama_name": "laundry" }, + { "id": "166", "name": "TV (local channels only)", "roomorama_name": "tv" }, + { "id": "167", "name": "satellite TV", "roomorama_name": "tv" }, + { "id": "174", "name": "internet connection", "roomorama_name": "internet" }, + { "id": "180", "name": "air conditioning", "roomorama_name": "airconditioning" }, + { "id": "227", "name": "swimming pool", "roomorama_name": "pool" }, + { "id": "234", "name": "Laundry", "roomorama_name": "laundry" }, + { "id": "235", "name": "Breakfast", "roomorama_name": "breakfast" }, + { "id": "281", "name": "Wheelchair access possible", "roomorama_name": "wheelchairaccess" }, + { "id": "294", "name": "A Gym is in the building for guests to use", "roomorama_name": "gym" }, + { "id": "295", "name": "On street parking", "roomorama_name": "parking" }, + { "id": "296", "name": "Underground parking", "roomorama_name": "parking" }, + { "id": "302", "name": "Guarded parking", "roomorama_name": "parking" }, + { "id": "325", "name": "DVD player", "roomorama_name": "tv" }, + { "id": "339", "name": "FREE internet access", "roomorama_name": "internet" }, + { "id": "371", "name": "Home Theatre", "roomorama_name": "tv" }, + { "id": "438", "name": "High speed Internet access", "roomorama_name": "internet" }, + { "id": "448", "name": "Free weekly cleaning", "roomorama_name": "free_cleaning" }, + { "id": "504", "name": "Private parking", "roomorama_name": "parking" }, + { "id": "589", "name": "Bed Linen", "roomorama_name": "bed_linen_and_towels" }, + { "id": "613", "name": "Laundry (Private)", "roomorama_name": "laundry" }, + { "id": "614", "name": "Laundry (Common)", "roomorama_name": "laundry" }, + { "id": "616", "name": "Doorman", "roomorama_name": "doorman" }, + { "id": "625", "name": "Garden (Private)", "roomorama_name": "outdoor_space" }, + { "id": "626", "name": "Garden (Common)", "roomorama_name": "outdoor_space" }, + { "id": "628", "name": "Laundry Service", "roomorama_name": "laundry" }, + { "id": "639", "name": "Meal Plan - Bed and Breakfast", "roomorama_name": "breakfast" }, + { "id": "665", "name": "Continental Breakfast", "roomorama_name": "breakfast" }, + { "id": "689", "name": "Elevator", "roomorama_name": "elevator" }, + { "id": "697", "name": "Meal Plan - FAP/Full-board", "roomorama_name": "breakfast" }, + { "id": "698", "name": "Full English Breakfast", "roomorama_name": "breakfast" }, + { "id": "717", "name": "Meal Plan - MAP/Half-board", "roomorama_name": "breakfast" }, + { "id": "737", "name": "Heated Pool", "roomorama_name": "pool" }, + { "id": "738", "name": "Indoor Pool", "roomorama_name": "pool" }, + { "id": "739", "name": "Childrens Pool", "roomorama_name": "pool" }, + { "id": "740", "name": "Outdoor Pool", "roomorama_name": "pool" }, + { "id": "792", "name": "Wireless Internet", "roomorama_name": "wifi" }, + { "id": "793", "name": "Parking", "roomorama_name": "parking" }, + { "id": "794", "name": "Outdoor Parking", "roomorama_name": "parking" }, + { "id": "795", "name": "Valet Parking", "roomorama_name": "parking" }, + { "id": "803", "name": "Free parking on the street", "roomorama_name": "parking" }, + { "id": "804", "name": "Paid parking on the street", "roomorama_name": "parking" }, + { "id": "805", "name": "Free parking with garage", "roomorama_name": "parking" }, + { "id": "806", "name": "Paid parking with garage", "roomorama_name": "parking" }, + { "id": "807", "name": "Free cable internet", "roomorama_name": "internet" }, + { "id": "808", "name": "Paid cable internet", "roomorama_name": "internet" }, + { "id": "809", "name": "Paid wireless internet", "roomorama_name": "wifi" }, + { "id": "815", "name": "Communal pool", "roomorama_name": "pool" }, + { "id": "816", "name": "Private pool", "roomorama_name": "pool" }, + { "id": "826", "name": "Shared Swimming Pool", "roomorama_name": "pool" }, + { "id": "834", "name": "Gym", "roomorama_name": "gym" }, + { "id": "851", "name": "TV 3D", "roomorama_name": "tv" }, + { "id": "852", "name": "Smart TV", "roomorama_name": "tv" } + ], + + "disabled-amenities": [ + { "id": "3", "name": "Crockery & Cutlery", "roomorama_name": "" }, + { "id": "4", "name": "Iron & Ironing Board", "roomorama_name": "" }, + { "id": "5", "name": "Drying Rack", "roomorama_name": "" }, + { "id": "6", "name": "Hair Dryer", "roomorama_name": "" }, + { "id": "8", "name": "Toiletries", "roomorama_name": "" }, + { "id": "13", "name": "Dishwasher", "roomorama_name": "" }, + { "id": "17", "name": "kettle", "roomorama_name": "" }, + { "id": "21", "name": "Alarm Clock", "roomorama_name": "" }, + { "id": "22", "name": "Stereo", "roomorama_name": "" }, + { "id": "24", "name": "CD player", "roomorama_name": "" }, + { "id": "29", "name": "bidet", "roomorama_name": "" }, + { "id": "32", "name": "cupboard", "roomorama_name": "" }, + { "id": "33", "name": "vanity cupboard", "roomorama_name": "" }, + { "id": "37", "name": "toilet", "roomorama_name": "" }, + { "id": "61", "name": "double bed", "roomorama_name": "" }, + { "id": "66", "name": "built-in wardrobes", "roomorama_name": "" }, + { "id": "70", "name": "night table", "roomorama_name": "" }, + { "id": "71", "name": "night tables", "roomorama_name": "" }, + { "id": "72", "name": "reading lamps", "roomorama_name": "" }, + { "id": "73", "name": "desk", "roomorama_name": "" }, + { "id": "78", "name": "chest of drawers", "roomorama_name": "" }, + { "id": "81", "name": "Bathroom", "roomorama_name": "" }, + { "id": "99", "name": "Lounge", "roomorama_name": "" }, + { "id": "100", "name": "Terrace", "roomorama_name": "" }, + { "id": "114", "name": "cooking hob", "roomorama_name": "" }, + { "id": "115", "name": "oven", "roomorama_name": "" }, + { "id": "119", "name": "cooker", "roomorama_name": "" }, + { "id": "123", "name": "electric kettle", "roomorama_name": "" }, + { "id": "124", "name": "microwave", "roomorama_name": "" }, + { "id": "125", "name": "toaster", "roomorama_name": "" }, + { "id": "128", "name": "plates", "roomorama_name": "" }, + { "id": "129", "name": "pans", "roomorama_name": "" }, + { "id": "130", "name": "fridge / freezer", "roomorama_name": "" }, + { "id": "131", "name": "fridge", "roomorama_name": "" }, + { "id": "140", "name": "coffee maker", "roomorama_name": "" }, + { "id": "142", "name": "dish rack", "roomorama_name": "" }, + { "id": "143", "name": "vacuum cleaner", "roomorama_name": "" }, + { "id": "146", "name": "gas/electric hob", "roomorama_name": "" }, + { "id": "150", "name": "breakfast bar and stools", "roomorama_name": "" }, + { "id": "152", "name": "freezer", "roomorama_name": "" }, + { "id": "157", "name": "kitchenette", "roomorama_name": "" }, + { "id": "161", "name": "armchairs", "roomorama_name": "" }, + { "id": "163", "name": "coffee table", "roomorama_name": "" }, + { "id": "182", "name": "sofa", "roomorama_name": "" }, + { "id": "187", "name": "heating", "roomorama_name": "" }, + { "id": "188", "name": "lamp", "roomorama_name": "" }, + { "id": "189", "name": "table and chairs", "roomorama_name": "" }, + { "id": "197", "name": "shelves", "roomorama_name": "" }, + { "id": "198", "name": "radio", "roomorama_name": "" }, + { "id": "200", "name": "double sofa bed", "roomorama_name": "" }, + { "id": "201", "name": "wardrobe", "roomorama_name": "" }, + { "id": "203", "name": "double sofa", "roomorama_name": "" }, + { "id": "205", "name": "Help Desk", "roomorama_name": "" }, + { "id": "208", "name": "High Chairs", "roomorama_name": "" }, + { "id": "209", "name": "Extra Bed", "roomorama_name": "" }, + { "id": "210", "name": "Mattress", "roomorama_name": "" }, + { "id": "215", "name": "Airport Pick-up Service", "roomorama_name": "" }, + { "id": "225", "name": "Maid Service", "roomorama_name": "" }, + { "id": "232", "name": "Dry Cleaning & Laundry", "roomorama_name": "" }, + { "id": "233", "name": "Dry Cleaning", "roomorama_name": "" }, + { "id": "237", "name": "sofabed", "roomorama_name": "" }, + { "id": "239", "name": "shower", "roomorama_name": "" }, + { "id": "245", "name": "washbasin ", "roomorama_name": "" }, + { "id": "250", "name": "dining table", "roomorama_name": "" }, + { "id": "253", "name": "jacuzzi", "roomorama_name": "" }, + { "id": "258", "name": "stove", "roomorama_name": "" }, + { "id": "261", "name": "Fan", "roomorama_name": "" }, + { "id": "312", "name": "Fax Machine", "roomorama_name": "" }, + { "id": "315", "name": "bathtub", "roomorama_name": "" }, + { "id": "323", "name": "single bed", "roomorama_name": "" }, + { "id": "324", "name": "king size bed", "roomorama_name": "" }, + { "id": "330", "name": "city maps", "roomorama_name": "" }, + { "id": "346", "name": "garden", "roomorama_name": "" }, + { "id": "349", "name": "Wood burning fireplace", "roomorama_name": "" }, + { "id": "355", "name": "Upon weekly stays: maidservice including personal laundry / ironing", "roomorama_name": "" }, + { "id": "363", "name": "armchair", "roomorama_name": "" }, + { "id": "364", "name": "Fireplace", "roomorama_name": "" }, + { "id": "365", "name": "Sauna", "roomorama_name": "" }, + { "id": "374", "name": "Cell Phone Rentals", "roomorama_name": "" }, + { "id": "380", "name": "Complimentary Tea & Coffee", "roomorama_name": "" }, + { "id": "390", "name": "Espresso-Machine", "roomorama_name": "" }, + { "id": "391", "name": "Hot Tub", "roomorama_name": "" }, + { "id": "395", "name": "Towels", "roomorama_name": "" }, + { "id": "404", "name": "Health Club", "roomorama_name": "" }, + { "id": "408", "name": "BBQ grill", "roomorama_name": "" }, + { "id": "409", "name": "en suite bathroom", "roomorama_name": "" }, + { "id": "413", "name": "Ice Maker", "roomorama_name": "" }, + { "id": "418", "name": "Video game system", "roomorama_name": "" }, + { "id": "421", "name": "Game room", "roomorama_name": "" }, + { "id": "429", "name": "Computer rental", "roomorama_name": "" }, + { "id": "436", "name": "blender", "roomorama_name": "" }, + { "id": "439", "name": "dining room", "roomorama_name": "" }, + { "id": "440", "name": "Pair of twin beds", "roomorama_name": "" }, + { "id": "444", "name": "Bunk Bed", "roomorama_name": "" }, + { "id": "447", "name": "Heated towel bar", "roomorama_name": "" }, + { "id": "450", "name": "street", "roomorama_name": "" }, + { "id": "451", "name": "Courtyard", "roomorama_name": "" }, + { "id": "453", "name": "Annex Room", "roomorama_name": "" }, + { "id": "456", "name": "chairs", "roomorama_name": "" }, + { "id": "461", "name": "Business centre", "roomorama_name": "" }, + { "id": "478", "name": "Downtown", "roomorama_name": "" }, + { "id": "485", "name": "Queen size bed", "roomorama_name": "" }, + { "id": "490", "name": "Fan(s) on request", "roomorama_name": "" }, + { "id": "491", "name": "Iron/Ironing board on request", "roomorama_name": "" }, + { "id": "497", "name": "Luggage Storage Facilities", "roomorama_name": "" }, + { "id": "503", "name": "mirror", "roomorama_name": "" }, + { "id": "506", "name": "cupboards", "roomorama_name": "" }, + { "id": "507", "name": "Table", "roomorama_name": "" }, + { "id": "508", "name": "Chair", "roomorama_name": "" }, + { "id": "511", "name": "Sea", "roomorama_name": "" }, + { "id": "516", "name": "En suite shower", "roomorama_name": "" }, + { "id": "590", "name": "Iron", "roomorama_name": "" }, + { "id": "591", "name": "Ironing Board", "roomorama_name": "" }, + { "id": "592", "name": "Telephone", "roomorama_name": "" }, + { "id": "593", "name": "Computer", "roomorama_name": "" }, + { "id": "594", "name": "Canal view", "roomorama_name": "" }, + { "id": "595", "name": "Pets are welcome", "roomorama_name": "" }, + { "id": "596", "name": "Free cot in the apartment", "roomorama_name": "" }, + { "id": "597", "name": "Free cot on request", "roomorama_name": "" }, + { "id": "598", "name": "Concierge", "roomorama_name": "" }, + { "id": "599", "name": "Washer dryer", "roomorama_name": "" }, + { "id": "600", "name": "Dryer", "roomorama_name": "" }, + { "id": "601", "name": "Safe", "roomorama_name": "" }, + { "id": "602", "name": "Ski Storage", "roomorama_name": "" }, + { "id": "603", "name": "Mountain View", "roomorama_name": "" }, + { "id": "604", "name": "Seaview", "roomorama_name": "" }, + { "id": "605", "name": "Fitness Room", "roomorama_name": "" }, + { "id": "606", "name": "Spa", "roomorama_name": "" }, + { "id": "607", "name": "Steam room", "roomorama_name": "" }, + { "id": "608", "name": "Room Service", "roomorama_name": "" }, + { "id": "609", "name": "Slope View", "roomorama_name": "" }, + { "id": "611", "name": "Hot Tub (Private)", "roomorama_name": "" }, + { "id": "612", "name": "Hot Tub (Common)", "roomorama_name": "" }, + { "id": "615", "name": "Minibar", "roomorama_name": "" }, + { "id": "617", "name": "Breakfast Room", "roomorama_name": "" }, + { "id": "618", "name": "Meeting Room", "roomorama_name": "" }, + { "id": "619", "name": "Restaurant", "roomorama_name": "" }, + { "id": "620", "name": "Bar", "roomorama_name": "" }, + { "id": "621", "name": "Beauty Salon", "roomorama_name": "" }, + { "id": "622", "name": "Children Area", "roomorama_name": "" }, + { "id": "623", "name": "Ski In and Out", "roomorama_name": "" }, + { "id": "624", "name": "Pull-Out Bed", "roomorama_name": "" }, + { "id": "627", "name": "Reception", "roomorama_name": "" }, + { "id": "629", "name": "Adjoining Rooms", "roomorama_name": "" }, + { "id": "630", "name": "Outlet Adapters", "roomorama_name": "" }, + { "id": "631", "name": "Airline Desk", "roomorama_name": "" }, + { "id": "632", "name": "Meal Plan - American", "roomorama_name": "" }, + { "id": "633", "name": "ATM/Cash Machine", "roomorama_name": "" }, + { "id": "634", "name": "Audio Visual Equipment", "roomorama_name": "" }, + { "id": "635", "name": "Babysitting/Child Services", "roomorama_name": "" }, + { "id": "636", "name": "Barber Shop", "roomorama_name": "" }, + { "id": "637", "name": "On The Bay", "roomorama_name": "" }, + { "id": "638", "name": "Bay View", "roomorama_name": "" }, + { "id": "640", "name": "Baby Listening Device", "roomorama_name": "" }, + { "id": "641", "name": "Beach View", "roomorama_name": "" }, + { "id": "642", "name": "Beach", "roomorama_name": "" }, + { "id": "643", "name": "Barber/Beauty Shop", "roomorama_name": "" }, + { "id": "644", "name": "Porters", "roomorama_name": "" }, + { "id": "645", "name": "Bicycle Rentals", "roomorama_name": "" }, + { "id": "646", "name": "Blackboard", "roomorama_name": "" }, + { "id": "647", "name": "Billiards / Pool Tables", "roomorama_name": "" }, + { "id": "648", "name": "Boating", "roomorama_name": "" }, + { "id": "649", "name": "Boutiques", "roomorama_name": "" }, + { "id": "650", "name": "Bowling", "roomorama_name": "" }, + { "id": "651", "name": "Meal Plan - Bermuda", "roomorama_name": "" }, + { "id": "652", "name": "Braille Elevator", "roomorama_name": "" }, + { "id": "653", "name": "Breakfast Buffet", "roomorama_name": "" }, + { "id": "654", "name": "Bathroom Telephone", "roomorama_name": "" }, + { "id": "655", "name": "Canopy / Poster Bed", "roomorama_name": "" }, + { "id": "656", "name": "Car Rental Desk", "roomorama_name": "" }, + { "id": "657", "name": "Casino", "roomorama_name": "" }, + { "id": "658", "name": "Castle Room", "roomorama_name": "" }, + { "id": "659", "name": "Meal Plan - Caribbean", "roomorama_name": "" }, + { "id": "660", "name": "CD Player", "roomorama_name": "" }, + { "id": "661", "name": "Ceiling Fan", "roomorama_name": "" }, + { "id": "662", "name": "City View", "roomorama_name": "" }, + { "id": "663", "name": "Conference Facilities", "roomorama_name": "" }, + { "id": "664", "name": "Conference Suite", "roomorama_name": "" }, + { "id": "666", "name": "Coffee Shop", "roomorama_name": "" }, + { "id": "667", "name": "Coffee Maker in Room", "roomorama_name": "" }, + { "id": "668", "name": "Computer in Room", "roomorama_name": "" }, + { "id": "669", "name": "Concierge Desk", "roomorama_name": "" }, + { "id": "670", "name": "Connecting Rooms", "roomorama_name": "" }, + { "id": "671", "name": "Meal Plan - Continental", "roomorama_name": "" }, + { "id": "672", "name": "Copy Service", "roomorama_name": "" }, + { "id": "673", "name": "Cordless Phone", "roomorama_name": "" }, + { "id": "674", "name": "Cribs Available", "roomorama_name": "" }, + { "id": "675", "name": "Courtesy Car", "roomorama_name": "" }, + { "id": "676", "name": "City Center", "roomorama_name": "" }, + { "id": "677", "name": "Currency Exchange", "roomorama_name": "" }, + { "id": "678", "name": "Data port Available", "roomorama_name": "" }, + { "id": "679", "name": "24 Hour Front Desk", "roomorama_name": "" }, + { "id": "680", "name": "Dining Guide", "roomorama_name": "" }, + { "id": "681", "name": "Dinner", "roomorama_name": "" }, + { "id": "682", "name": "Handicapped Rooms/Facilities", "roomorama_name": "" }, + { "id": "683", "name": "Disco", "roomorama_name": "" }, + { "id": "684", "name": "Doctor on Call", "roomorama_name": "" }, + { "id": "685", "name": "Drugstore", "roomorama_name": "" }, + { "id": "686", "name": "Driving Range", "roomorama_name": "" }, + { "id": "687", "name": "Desk with lamp", "roomorama_name": "" }, + { "id": "688", "name": "Electronic Door Locks", "roomorama_name": "" }, + { "id": "690", "name": "Email Service", "roomorama_name": "" }, + { "id": "691", "name": "Live Entertainment", "roomorama_name": "" }, + { "id": "692", "name": "Meal Plan - European", "roomorama_name": "" }, + { "id": "693", "name": "Express Check In", "roomorama_name": "" }, + { "id": "694", "name": "Executive Desk", "roomorama_name": "" }, + { "id": "695", "name": "Express Checkout", "roomorama_name": "" }, + { "id": "696", "name": "Executive Level", "roomorama_name": "" }, + { "id": "699", "name": "Female Executive Rooms", "roomorama_name": "" }, + { "id": "700", "name": "Fishing", "roomorama_name": "" }, + { "id": "701", "name": "Florist", "roomorama_name": "" }, + { "id": "702", "name": "Free Parking", "roomorama_name": "" }, + { "id": "703", "name": "Free Local Telephone Calls", "roomorama_name": "" }, + { "id": "704", "name": "Free Transportation", "roomorama_name": "" }, + { "id": "705", "name": "Garden View", "roomorama_name": "" }, + { "id": "706", "name": "Gift Shop", "roomorama_name": "" }, + { "id": "707", "name": "Game Rental", "roomorama_name": "" }, + { "id": "708", "name": "Golf", "roomorama_name": "" }, + { "id": "709", "name": "Golf Course View", "roomorama_name": "" }, + { "id": "710", "name": "Horseback Riding", "roomorama_name": "" }, + { "id": "711", "name": "Jogging Track", "roomorama_name": "" }, + { "id": "712", "name": "Kennels", "roomorama_name": "" }, + { "id": "713", "name": "Childrens Activities", "roomorama_name": "" }, + { "id": "714", "name": "Lake View", "roomorama_name": "" }, + { "id": "715", "name": "Guest Laundromat", "roomorama_name": "" }, + { "id": "716", "name": "Lunch", "roomorama_name": "" }, + { "id": "718", "name": "Massage", "roomorama_name": "" }, + { "id": "719", "name": "Miniature Golf", "roomorama_name": "" }, + { "id": "720", "name": "In Room Movies", "roomorama_name": "" }, + { "id": "721", "name": "Meeting Facilities", "roomorama_name": "" }, + { "id": "722", "name": "Meeting Suite", "roomorama_name": "" }, + { "id": "723", "name": "Multilingual", "roomorama_name": "" }, + { "id": "724", "name": "Nursery for Children", "roomorama_name": "" }, + { "id": "725", "name": "No Smoking Rooms/Facilities", "roomorama_name": "" }, + { "id": "726", "name": "Night Club", "roomorama_name": "" }, + { "id": "727", "name": "Free Newspaper", "roomorama_name": "" }, + { "id": "728", "name": "News Stand", "roomorama_name": "" }, + { "id": "729", "name": "Ocean View", "roomorama_name": "" }, + { "id": "730", "name": "Overhead Projector", "roomorama_name": "" }, + { "id": "731", "name": "Parasailing", "roomorama_name": "" }, + { "id": "732", "name": "Park View", "roomorama_name": "" }, + { "id": "733", "name": "No Pets Allowed", "roomorama_name": "" }, + { "id": "734", "name": "Phone Service", "roomorama_name": "" }, + { "id": "735", "name": "Picnic Area/Tables", "roomorama_name": "" }, + { "id": "736", "name": "Play Ground", "roomorama_name": "" }, + { "id": "741", "name": "Poolside Snackbar", "roomorama_name": "" }, + { "id": "742", "name": "Projector", "roomorama_name": "" }, + { "id": "743", "name": "Squash", "roomorama_name": "" }, + { "id": "744", "name": "River View", "roomorama_name": "" }, + { "id": "745", "name": "Ramp Access to Buildings", "roomorama_name": "" }, + { "id": "746", "name": "24 Hour Room Service", "roomorama_name": "" }, + { "id": "747", "name": "Safe Deposit", "roomorama_name": "" }, + { "id": "748", "name": "Sailing", "roomorama_name": "" }, + { "id": "749", "name": "Scuba Diving", "roomorama_name": "" }, + { "id": "750", "name": "Secretarial Service", "roomorama_name": "" }, + { "id": "751", "name": "24 Hour Security", "roomorama_name": "" }, + { "id": "752", "name": "Shopping Mall", "roomorama_name": "" }, + { "id": "753", "name": "Free Airport Shuttle", "roomorama_name": "" }, + { "id": "754", "name": "Skeet Shooting", "roomorama_name": "" }, + { "id": "755", "name": "Skiing", "roomorama_name": "" }, + { "id": "756", "name": "Cross Country Skiing", "roomorama_name": "" }, + { "id": "757", "name": "Snorkeling", "roomorama_name": "" }, + { "id": "758", "name": "Snowboarding", "roomorama_name": "" }, + { "id": "759", "name": "Fitness Center or Spa", "roomorama_name": "" }, + { "id": "760", "name": "Steam Bath", "roomorama_name": "" }, + { "id": "761", "name": "Telex", "roomorama_name": "" }, + { "id": "762", "name": "Indoor Tennis", "roomorama_name": "" }, + { "id": "763", "name": "Tennis", "roomorama_name": "" }, + { "id": "764", "name": "Outdoor Tennis", "roomorama_name": "" }, + { "id": "765", "name": "Tour Desk", "roomorama_name": "" }, + { "id": "766", "name": "Translation Service", "roomorama_name": "" }, + { "id": "767", "name": "Laundry Services", "roomorama_name": "" }, + { "id": "768", "name": "Vending Machines", "roomorama_name": "" }, + { "id": "769", "name": "VIP Rooms/Services", "roomorama_name": "" }, + { "id": "770", "name": "Volleyball", "roomorama_name": "" }, + { "id": "771", "name": "Wake-up Service", "roomorama_name": "" }, + { "id": "772", "name": "Wedding Services", "roomorama_name": "" }, + { "id": "773", "name": "Wind Surfing", "roomorama_name": "" }, + { "id": "774", "name": "Water Skiing", "roomorama_name": "" }, + { "id": "775", "name": "Grab Bars in Bathroom", "roomorama_name": "" }, + { "id": "776", "name": "Heated Guest Rooms", "roomorama_name": "" }, + { "id": "777", "name": "Modem in Room", "roomorama_name": "" }, + { "id": "778", "name": "Murphy Bed", "roomorama_name": "" }, + { "id": "779", "name": "Rollaway Beds", "roomorama_name": "" }, + { "id": "780", "name": "Bathrobes", "roomorama_name": "" }, + { "id": "781", "name": "Smoke Detectors", "roomorama_name": "" }, + { "id": "782", "name": "Solarium", "roomorama_name": "" }, + { "id": "783", "name": "Sprinklers In Rooms", "roomorama_name": "" }, + { "id": "784", "name": "Theater Desk", "roomorama_name": "" }, + { "id": "785", "name": "Temperature Control", "roomorama_name": "" }, + { "id": "786", "name": "Trouser Press", "roomorama_name": "" }, + { "id": "788", "name": "Ipod Dock", "roomorama_name": "" }, + { "id": "789", "name": "Heating", "roomorama_name": "" }, + { "id": "790", "name": "Hi-Fi", "roomorama_name": "" }, + { "id": "791", "name": "Duty Free Shop", "roomorama_name": "" }, + { "id": "796", "name": "Prayer Mats", "roomorama_name": "" }, + { "id": "797", "name": "Racquetball Courts", "roomorama_name": "" }, + { "id": "798", "name": "Refrigerator", "roomorama_name": "" }, + { "id": "799", "name": "Smoking", "roomorama_name": "" }, + { "id": "800", "name": "Chef Provided", "roomorama_name": "" }, + { "id": "801", "name": "Marina View", "roomorama_name": "" }, + { "id": "802", "name": "Smoking allowed", "roomorama_name": "" }, + { "id": "810", "name": "Paid cot on request", "roomorama_name": "" }, + { "id": "811", "name": "Petanque", "roomorama_name": "" }, + { "id": "812", "name": "Ask for smoking", "roomorama_name": "" }, + { "id": "813", "name": "Ask for pets", "roomorama_name": "" }, + { "id": "814", "name": "Ask for accessibility", "roomorama_name": "" }, + { "id": "817", "name": "Ping-pong table", "roomorama_name": "" }, + { "id": "818", "name": "Breakfast booking possible", "roomorama_name": "" }, + { "id": "819", "name": "House cleaning optional", "roomorama_name": "" }, + { "id": "820", "name": "Sports - swimming", "roomorama_name": "" }, + { "id": "821", "name": "Local hospital", "roomorama_name": "" }, + { "id": "822", "name": "Local groceries", "roomorama_name": "" }, + { "id": "823", "name": "Waterfront", "roomorama_name": "" }, + { "id": "824", "name": "Near ocean", "roomorama_name": "" }, + { "id": "825", "name": "Shared Kitchen", "roomorama_name": "" }, + { "id": "827", "name": "garage", "roomorama_name": "" }, + { "id": "828", "name": "Family/kids friendly", "roomorama_name": "" }, + { "id": "830", "name": "Juicer", "roomorama_name": "" }, + { "id": "831", "name": "Security camera at entrance", "roomorama_name": "" }, + { "id": "832", "name": "MP3", "roomorama_name": "" }, + { "id": "833", "name": "Baby cot", "roomorama_name": "" }, + { "id": "835", "name": "Paddle", "roomorama_name": "" }, + { "id": "836", "name": "Taxi access", "roomorama_name": "" }, + { "id": "837", "name": "Rooftop access", "roomorama_name": "" }, + { "id": "838", "name": "Baby high chair", "roomorama_name": "" }, + { "id": "839", "name": "Baby cot paid", "roomorama_name": "" }, + { "id": "840", "name": "Baby chair on request", "roomorama_name": "" }, + { "id": "841", "name": "High chair on request", "roomorama_name": "" }, + { "id": "842", "name": "Pets paid", "roomorama_name": "" }, + { "id": "843", "name": "Pets accepted under request", "roomorama_name": "" }, + { "id": "844", "name": "Stroller", "roomorama_name": "" }, + { "id": "845", "name": "Baby cutlery", "roomorama_name": "" }, + { "id": "846", "name": "Internet connection on request", "roomorama_name": "" }, + { "id": "847", "name": "Cable TV on request", "roomorama_name": "" }, + { "id": "848", "name": "Dry cleaning on request", "roomorama_name": "" }, + { "id": "849", "name": "Free international calls", "roomorama_name": "" }, + { "id": "850", "name": "Chimney", "roomorama_name": "" }, + { "id": "853", "name": "Free car", "roomorama_name": "" }, + { "id": "854", "name": "Free bike", "roomorama_name": "" }, + { "id": "855", "name": "Credit card payment accepted", "roomorama_name": "" }, + { "id": "856", "name": "No parties", "roomorama_name": "" }, + { "id": "857", "name": "No children under 4", "roomorama_name": "" }, + { "id": "858", "name": "No children under 12", "roomorama_name": "" }, + { "id": "859", "name": "Anyone under 25 years", "roomorama_name": "" }, + { "id": "860", "name": "Anyone under 30 years", "roomorama_name": "" }, + { "id": "861", "name": "Anyone under 35 years", "roomorama_name": "" }, + { "id": "862", "name": "Anyone under 18 years", "roomorama_name": "" }, + { "id": "863", "name": "No reservation more than 30 days", "roomorama_name": "" }, + { "id": "864", "name": "Groups under 18 years", "roomorama_name": "" }, + { "id": "865", "name": "Groups under 25 years", "roomorama_name": "" }, + { "id": "866", "name": "Groups under 30 years", "roomorama_name": "" }, + { "id": "867", "name": "Groups under 35 years", "roomorama_name": "" }, + { "id": "868", "name": "Only families", "roomorama_name": "" }, + { "id": "869", "name": "Anyone under 40 years", "roomorama_name": "" }, + { "id": "870", "name": "No children under 6", "roomorama_name": "" }, + { "id": "871", "name": "Groups under 50 years", "roomorama_name": "" }, + { "id": "872", "name": "Same sex groups under 30 years", "roomorama_name": "" }, + { "id": "873", "name": "Groups under 45 years", "roomorama_name": "" }, + { "id": "874", "name": "Same sex groups under 35 years", "roomorama_name": "" }, + { "id": "875", "name": "Arrivals on Sunday", "roomorama_name": "" }, + { "id": "876", "name": "Families or couples only", "roomorama_name": "" }, + { "id": "877", "name": "Baby high chair paid", "roomorama_name": "" }, + { "id": "878", "name": "Baby chair paid", "roomorama_name": "" }, + { "id": "879", "name": "Wifi USB Adapter", "roomorama_name": "" }, + { "id": "880", "name": "Bottled water", "roomorama_name": "" }, + { "id": "881", "name": "Centrally controlled ventilation", "roomorama_name": "" }, + { "id": "882", "name": "Electrical adapters available", "roomorama_name": "" }, + { "id": "883", "name": "Hypoallergenic rooms", "roomorama_name": "" }, + { "id": "884", "name": "Internet browser TV", "roomorama_name": "" }, + { "id": "885", "name": "Power converters", "roomorama_name": "" }, + { "id": "886", "name": "Sewing kit", "roomorama_name": "" }, + { "id": "887", "name": "Printer", "roomorama_name": "" }, + { "id": "888", "name": "Slippers", "roomorama_name": "" }, + { "id": "889", "name": "Sound system", "roomorama_name": "" }, + { "id": "890", "name": "Sound proofed windows", "roomorama_name": "" }, + { "id": "891", "name": "Weighting scale", "roomorama_name": "" }, + { "id": "892", "name": "Run of the house", "roomorama_name": "" }, + { "id": "893", "name": "Window", "roomorama_name": "" }, + { "id": "894", "name": "Veranda", "roomorama_name": "" }, + { "id": "895", "name": "Patio", "roomorama_name": "" }, + { "id": "896", "name": "AC public areas", "roomorama_name": "" }, + { "id": "897", "name": "Aqua sports center", "roomorama_name": "" }, + { "id": "898", "name": "Courier service", "roomorama_name": "" }, + { "id": "899", "name": "Creche", "roomorama_name": "" }, + { "id": "900", "name": "Housekeeping service", "roomorama_name": "" }, + { "id": "901", "name": "Entertainment recreation", "roomorama_name": "" }, + { "id": "902", "name": "Diving", "roomorama_name": "" }, + { "id": "903", "name": "Hair salon", "roomorama_name": "" }, + { "id": "904", "name": "Hotel shops", "roomorama_name": "" }, + { "id": "905", "name": "Island hopping", "roomorama_name": "" }, + { "id": "906", "name": "Jet skiing", "roomorama_name": "" }, + { "id": "907", "name": "Kids eat for free", "roomorama_name": "" }, + { "id": "908", "name": "Late check-out available", "roomorama_name": "" }, + { "id": "909", "name": "Limo town car service available", "roomorama_name": "" }, + { "id": "910", "name": "Security guard", "roomorama_name": "" }, + { "id": "911", "name": "Shoe shine", "roomorama_name": "" }, + { "id": "912", "name": "Shoe polishing machine", "roomorama_name": "" }, + { "id": "913", "name": "Shuttle service", "roomorama_name": "" }, + { "id": "914", "name": "Suitable for children", "roomorama_name": "" }, + { "id": "915", "name": "Ticket service", "roomorama_name": "" }, + { "id": "916", "name": "Turndown service", "roomorama_name": "" }, + { "id": "917", "name": "Umbrella", "roomorama_name": "" }, + { "id": "918", "name": "Welcome amenities", "roomorama_name": "" } + ] +} diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/amenities.rb b/lib/concierge/suppliers/rentals_united/dictionaries/amenities.rb new file mode 100644 index 000000000..48e1a90c5 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/amenities.rb @@ -0,0 +1,77 @@ +module RentalsUnited + module Dictionaries + # +RentalsUnited::Dictionaries::Amenities+ + # + # This class is responsible for mapping amenitites between RU and + # Roomorama APIs. + class Amenities + attr_reader :facility_service_ids + + SMOKING_ALLOWED_IDS = ["799", "802"] + PETS_ALLOWED_IDS = ["595"] + + # Initialize amenities converter + # + # Arguments + # + # * +acility_service_ids+ [Array] + def initialize(facility_service_ids) + @facility_service_ids = Array(facility_service_ids) + end + + # Converts RU facility services to Roomorama amenities + # + # Returns +Array+ array with uniq supported amenitites + def convert + roomorama_amenities = facility_service_ids.map do |service_id| + amenity = self.class.find(service_id) + amenity["roomorama_name"] if amenity + end + + roomorama_amenities.compact.uniq + end + + def smoking_allowed? + facility_service_ids.any? { |id| SMOKING_ALLOWED_IDS.include?(id) } + end + + def pets_allowed? + facility_service_ids.any? { |id| PETS_ALLOWED_IDS.include?(id) } + end + + class << self + # Looks up for supported amenity by id. + # Returns nil if supported amenity was not found. + # + # Arguments + # + # * +service_id+ [String] rentals united id of facility service + # + # Returns a +Hash+ with mapping if supported facility service is found + # and +nil+ when there is no supported service with given id. + def find(service_id) + supported_amenities.find { |amenity| amenity["id"] == service_id } + end + + # Returns a hash with mapping between Rentals United facility services + # and Roomorama API supported amenities + # + # Returns an +Array+ with +Hash+ objects + def supported_amenities + @supported_amenities ||= load_amenities["enabled-amenities"] + end + + def load_amenities + JSON.parse(File.read(file_path)) + end + + def file_path + Hanami.root.join( + "lib/concierge/suppliers/rentals_united/dictionaries", + "amenities.json" + ).to_s + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms.json b/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms.json new file mode 100644 index 000000000..729810e4d --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms.json @@ -0,0 +1,33 @@ +[ + { "type_id": "1", "bedrooms": 0, "name": "Studio" }, + { "type_id": "2", "bedrooms": 1, "name": "One Bedroom" }, + { "type_id": "3", "bedrooms": 2, "name": "Two Bedroom" }, + { "type_id": "4", "bedrooms": 3, "name": "Three Bedroom" }, + { "type_id": "11", "bedrooms": 5, "name": "Five Bedroom" }, + { "type_id": "12", "bedrooms": 4, "name": "Four Bedroom" }, + { "type_id": "26", "bedrooms": 6, "name": "Six Bedroom" }, + { "type_id": "27", "bedrooms": 7, "name": "Seven Bedroom" }, + { "type_id": "28", "bedrooms": 8, "name": "Eight Bedroom" }, + { "type_id": "29", "bedrooms": 9, "name": "Nine Bedroom" }, + { "type_id": "30", "bedrooms": 10, "name": "Ten Bedroom" }, + { "type_id": "34", "bedrooms": 11, "name": "Eleven Bedroom" }, + { "type_id": "35", "bedrooms": 12, "name": "Twelve Bedroom" }, + { "type_id": "36", "bedrooms": 13, "name": "Thirteen Bedroom" }, + { "type_id": "37", "bedrooms": 14, "name": "Fourteen Bedroom" }, + { "type_id": "38", "bedrooms": 15, "name": "Fifteen Bedroom" }, + { "type_id": "39", "bedrooms": 16, "name": "Sixteen Bedroom" }, + { "type_id": "40", "bedrooms": 17, "name": "Seventeen Bedroom" }, + { "type_id": "41", "bedrooms": 18, "name": "Eighteen Bedroom" }, + { "type_id": "42", "bedrooms": 19, "name": "Nineteen Bedroom" }, + { "type_id": "43", "bedrooms": 20, "name": "Twenty Bedroom" }, + { "type_id": "44", "bedrooms": 21, "name": "Twentyone Bedroom" }, + { "type_id": "45", "bedrooms": 22, "name": "Twentytwo Bedroom" }, + { "type_id": "46", "bedrooms": 23, "name": "Twentythree Bedroom" }, + { "type_id": "47", "bedrooms": 24, "name": "Twentyfour Bedroom" }, + { "type_id": "48", "bedrooms": 25, "name": "Twentyfive Bedroom" }, + { "type_id": "49", "bedrooms": 26, "name": "Twentysix Bedroom" }, + { "type_id": "50", "bedrooms": 27, "name": "Twentyseven Bedroom" }, + { "type_id": "51", "bedrooms": 28, "name": "Twentyeight Bedroom" }, + { "type_id": "52", "bedrooms": 29, "name": "Twentynine Bedroom" }, + { "type_id": "53", "bedrooms": 30, "name": "Thirty Bedroom" } +] diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms.rb b/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms.rb new file mode 100644 index 000000000..040bc44c8 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms.rb @@ -0,0 +1,40 @@ +module RentalsUnited + module Dictionaries + # +RentalsUnited::Dictionaries::Bedrooms+ + # + # This class is responsible for bedrooms count mapping to bedrooms type ids + class Bedrooms + class << self + # Find bedrooms count by bedroom type id + # + # Arguments + # + # * +id+ [String] id of bedroom type + # + # Usage + # + # RentalsUnited::Dictionaries::Bedrooms.count_by_type_id("1") + # + # Returns [Integer] bedrooms count + def count_by_type_id(id) + type = bedrooms.find { |bedroom| bedroom["type_id"] == id } + return nil unless type + + type["bedrooms"] + end + + private + def bedrooms + JSON.parse(File.read(file_path)) + end + + def file_path + Hanami.root.join( + "lib/concierge/suppliers/rentals_united/dictionaries", + "bedrooms.json" + ).to_s + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/property_types.json b/lib/concierge/suppliers/rentals_united/dictionaries/property_types.json new file mode 100644 index 000000000..84e3caade --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/property_types.json @@ -0,0 +1,14 @@ +[ + { "id": "3", "rentals_united_name": "Apartment", "roomorama_name": "apartment", "roomorama_subtype_name": null }, + { "id": "4", "rentals_united_name": "Bed and breakfast", "roomorama_name": "bnb", "roomorama_subtype_name": null }, + { "id": "7", "rentals_united_name": "Chalet", "roomorama_name": "house", "roomorama_subtype_name": null }, + { "id": "16", "rentals_united_name": "Guest house", "roomorama_name": "house", "roomorama_subtype_name": null }, + { "id": "20", "rentals_united_name": "Hotel", "roomorama_name": null, "roomorama_subtype_name": null }, + { "id": "30", "rentals_united_name": "Resort", "roomorama_name": "hotel", "roomorama_subtype_name": "resort" }, + { "id": "35", "rentals_united_name": "Villa", "roomorama_name": "house", "roomorama_subtype_name": "villa" }, + { "id": "37", "rentals_united_name": "Castle", "roomorama_name": "house", "roomorama_subtype_name": "chateau" }, + { "id": "63", "rentals_united_name": "Aparthotel", "roomorama_name": "apartment", "roomorama_subtype_name": null }, + { "id": "64", "rentals_united_name": "Boat", "roomorama_name": null, "roomorama_subtype_name": null }, + { "id": "65", "rentals_united_name": "Cottage", "roomorama_name": "house", "roomorama_subtype_name": "cottage" }, + { "id": "66", "rentals_united_name": "Camping", "roomorama_name": null, "roomorama_subtype_name": null } +] diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/property_types.rb b/lib/concierge/suppliers/rentals_united/dictionaries/property_types.rb new file mode 100644 index 000000000..d44f2bb01 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/property_types.rb @@ -0,0 +1,58 @@ +module RentalsUnited + module Dictionaries + # +RentalsUnited::Dictionaries::PropertyTypes+ + # + # This class is responsible for mapping property types between RU and + # Roomorama APIs. + class PropertyTypes + class << self + # Find property type by its id + # + # Arguments + # + # * +id+ [String] id of property type + # + # Usage + # + # RentalsUnited::Dictionaries::PropertyTypes.find("35") + # + # Returns [Entities::PropertyType] property type object + def find(id) + property_type = all.find { |p| p.id == id } + + if property_type + return nil if property_type.roomorama_name.nil? + + property_type + end + end + + # Find all property types + # + # Returns [Array] array of property types + def all + @all ||= property_hashes.map do |hash| + Entities::PropertyType.new( + id: hash["id"], + name: hash["rentals_united_name"], + roomorama_name: hash["roomorama_name"], + roomorama_subtype_name: hash["roomorama_subtype_name"] + ) + end + end + + private + def property_hashes + JSON.parse(File.read(file_path)) + end + + def file_path + Hanami.root.join( + "lib/concierge/suppliers/rentals_united/dictionaries", + "property_types.json" + ).to_s + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/statuses.json b/lib/concierge/suppliers/rentals_united/dictionaries/statuses.json new file mode 100644 index 000000000..b82e633a3 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/statuses.json @@ -0,0 +1,133 @@ +{ + "0": "Success", + "1": "Property is not available for a given dates", + "10": "Could not insert early departure fee, From:{0} To:{1} Fee:{2}", + "100": "Property description is required", + "101": "Pets not allowed", + "102": "Currency doesn't match with city currency", + "103": "Properties collection cannot be empty", + "104": "You need provide at least one value to modify stay.", + "105": "Some periods overlap. Periods must be separable.", + "106": "You can only modify stay in confirmed reservation.", + "107": "No reserved apartment found.", + "108": "Client Price cannot be negative", + "109": "Already Paid cannot be negative.", + "11": "Wrong payment method id:{0}", + "110": "Can not use OwnerID created by other users.", + "111": "Only property owner can add reviews.", + "112": "Review rating value must be between 0-5", + "113": "Submitted date must be later than arrival date", + "114": "Cannot remove confirmed reservation. Some periods ignored.", + "115": "MinStays collection cannot be empty.", + "116": "LocationID {0} is not proper city location.", + "117": "Only one description allowed per language.", + "118": "Max number of guests must be of positive value.", + "119": "Property name is not defined.", + "12": "Wrong deposit type id:{0}", + "120": "Check-in / check-out details are incorrect.", + "121": "Reservation not mapped in PMS. Contact IT support with Rentals United ID and your PMS Reservation ID.", + "122": "Failed to modify reservation in PMS. Try again or contact IT support for more information.", + "123": "Failed to cancel reservation in PMS. Try again or contact IT support for more information.", + "124": "Failed to insert reservation in PMS. Try again or contact IT support for more information.", + "125": "Wrong quantity of amenities. It should be between 0 - 32767.", + "126": "Invalid URL.", + "127": "Missing mandatory element: {0}.", + "128": "Cancellation policy text cannot be empty.", + "129": "Only reservations for apartments from same city are allowed", + "13": "Cancallation policies overlaps", + "130": "Cannot change apartment from city other than initial reservation. Cancel this reservation and create new one.", + "14": "Owner does not exist", + "15": "Apartment name ({0}) already exist in database.", + "16": "You already defined apartment with PUID:{0}", + "17": "Unexpected error, contact IT or try again", + "18": "Property with given ID does not exist.", + "19": "Dates mishmash", + "2": "Nothing available for a given dates", + "20": "Past dates", + "21": "Weird block dates for property: {0} - {1} - {2}. Whole block is {3} - {4}", + "22": "We have confirmed reservation for those dates. Please cancel the reservation instead of marking the dates as available.", + "23": "Wrong ImageTypeID:{0}", + "24": "Your are not the owner of the apartment.", + "25": "The value of 'Bigger' must be smaller than the value of 'Smaller'.", + "26": "Warning! Look at Notifs collection.", + "27": "DaysToArrivalFrom and DaysToArrivalTo requires positive values.", + "28": "Reservation does not exist.", + "29": "Requested stay, cost details do not match with property on reservation on hold.", + "3": "Property has no price settings for a given dates", + "30": "Element ignored because of other errors.", + "31": "Error occured. All changes rolled back.", + "32": "Bigger and Smaller requires positive values.", + "33": "Smaller is smaller than Bigger.", + "34": "RUPrice is not valid. Correct price is:{0}", + "35": "AlreadyPaid is bigger than ClientPrice.", + "36": "Wrong DetailedLocationID. City or district precision is required.", + "37": "Property name is too long (max 50).", + "38": "Property has missing data and cannot be offered.", + "39": "Location does not exist.", + "4": "Wrong destination id:{0}", + "40": "You cannot define discounts before the prices. The property has missing prices in given dates.", + "41": "The reservation was created by the other user.", + "42": "The reservation is expired.", + "43": "You cannot confirm this reservation. It's broken.", + "44": "The apartments are not in the same city.", + "45": "Data validation error.", + "46": "The property is not active. PropertyID:{0}", + "47": "Property is not available for a given dates. PropertyID:{0}", + "48": "The reservation is not on Put On Hold status.", + "49": "CountryID does not exist.", + "5": "Wrong distance unit id:{0}", + "50": "Guest name is required.", + "51": "Guest surname is required.", + "52": "Guest email is required.", + "53": "This method is deprecated. Use Push_PutConfirmedReservationMulti_RS", + "54": "This method is deprecated. Use Push_PutPropertiesOnHold_RQ", + "55": "Negative values in price elements is not allowed.", + "56": "Property does not exist.", + "57": "The request contains both types of composition definitions: composition and composition with amenities. Please use only one type.", + "58": "This amenity: {0} is not allowed in room type: {1}", + "59": "Positive value is required", + "6": "Wrong composition room id:{0}", + "60": "Duplicate value in LOSS element", + "61": "Duplicate value in EGPS element", + "62": "Missing Text or Image value.", + "63": "Wrong laguage id:{0}.", + "64": "DayOfWeek attribute must be between {0} and {1}.", + "65": "No permission to property {0}.", + "66": "Coordinates are missing or are invalid.", + "67": "Duplicate value in LOSPS element", + "68": "NumberOfGuests in LOSP element has to be greather than 0", + "69": "Building does not exist", + "7": "Wrong amenity id:{0}", + "70": "Some properties not updated:{0}", + "71": "Wrong security deposit type id: {0}", + "72": "Discount value can't be lower than 0.", + "73": "At least one PropertyID element is required.", + "74": "DateFrom has to be earlier than DateTo.", + "75": "DateFrom has to be earlier or equal to DateTo.", + "76": "Number of guests exceedes the maximum allowed.", + "77": "NOP: positive value required.", + "78": "Minimum stay is not valid (X nights).", + "79": "Stay period doesn't match with minimum stay", + "8": "Wrong arrival instructions", + "80": "Cannot activate archived property", + "81": "You don't have permission to modify this owner", + "82": "Apartment is Archived or no longer available or not Active", + "83": "Mixed owners in the request. Contact IT.", + "84": "Too many properties in your request (max 100).", + "85": "Invalid time value. Allowed values 00:00 - 23:59", + "86": "Operation has reached the maximum limit of time. The results are not complete.", + "87": "Wrong page URL Type", + "88": "Wrong date format for parameter {0}", + "89": "Stay period doesn't match with changeover", + "9": "Could not insert late arrival fee, From:{0} To:{1} Fee:{2}", + "90": "Enqueued", + "91": "Not found", + "92": "Duplicate value in distances.", + "93": "Unauthorized", + "94": "Some of required fields were not filled.", + "95": "Email already exists.", + "96": "Password must be at least 8 characters long.", + "97": "Standard number of guests must be of positive value.", + "98": "Deposit amount can't exceed value of 214,748.3647", + "99": "Technical error - missing file" +} diff --git a/lib/concierge/suppliers/rentals_united/dictionaries/statuses.rb b/lib/concierge/suppliers/rentals_united/dictionaries/statuses.rb new file mode 100644 index 000000000..fe3aee106 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/dictionaries/statuses.rb @@ -0,0 +1,39 @@ +module RentalsUnited + module Dictionaries + # +RentalsUnited::Dictionaries::Statuses+ + # + # This class is responsible for mapping RentalsUnited error codes and + # descriptions. + class Statuses + class << self + # Return error descriptions by error code + # + # Arguments + # + # * +code+ [String] error code + # + # Usage + # + # RentalsUnited::Dictionaries::Statuses.find("1") + # => "Property is not available for a given dates" + # + # Returns [String] error description + def find(code) + statuses_hash[code] + end + + private + def statuses_hash + @statuses_hash ||= JSON.parse(File.read(file_path)) + end + + def file_path + Hanami.root.join( + "lib/concierge/suppliers/rentals_united/dictionaries", + "statuses.json" + ).to_s + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/availability.rb b/lib/concierge/suppliers/rentals_united/entities/availability.rb new file mode 100644 index 000000000..6cd46f316 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/availability.rb @@ -0,0 +1,17 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::Availability+ + # + # This entity represents an availability type object. + class Availability + attr_accessor :date, :available, :minimum_stay, :changeover + + def initialize(date:, available:, minimum_stay:, changeover:) + @date = date + @available = available + @minimum_stay = minimum_stay + @changeover = changeover + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/location.rb b/lib/concierge/suppliers/rentals_united/entities/location.rb new file mode 100644 index 000000000..12b71117c --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/location.rb @@ -0,0 +1,22 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::Location+ + # + # This entity represents a location type object. + class Location + attr_accessor :id, :neighborhood, :city, :region, :country, :currency + + def initialize(id) + @id = id + end + + def load(attrs) + self.neighborhood = attrs[:neighborhood] + self.city = attrs[:city] + self.region = attrs[:region] + self.country = attrs[:country] + self + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/owner.rb b/lib/concierge/suppliers/rentals_united/entities/owner.rb new file mode 100644 index 000000000..66d978939 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/owner.rb @@ -0,0 +1,18 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::Owner+ + # + # This entity represents an owner object. + class Owner + attr_accessor :id, :first_name, :last_name, :email, :phone + + def initialize(id:, first_name:, last_name:, email:, phone:) + @id = id + @first_name = first_name + @last_name = last_name + @email = email + @phone = phone + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/price.rb b/lib/concierge/suppliers/rentals_united/entities/price.rb new file mode 100644 index 000000000..59280d1f1 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/price.rb @@ -0,0 +1,19 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::Price+ + # + # This entity represents a price object. + class Price + attr_accessor :total + + def initialize(total:, available:) + @total = total + @available = available + end + + def available? + @available + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/properties_collection.rb b/lib/concierge/suppliers/rentals_united/entities/properties_collection.rb new file mode 100644 index 000000000..0c0762d58 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/properties_collection.rb @@ -0,0 +1,34 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::PropertiesCollection+ + # + # This entity represents a properties collection object. + class PropertiesCollection + def initialize(entries) + @entries = entries + end + + def each_entry + if block_given? + @entries.each do |e| + yield(e[:property_id], e[:location_id]) + end + else + return @entries.each + end + end + + def size + @entries.size + end + + def property_ids + @entries.map { |e| e[:property_id] } + end + + def location_ids + @entries.map { |e| e[:location_id] }.uniq + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/property.rb b/lib/concierge/suppliers/rentals_united/entities/property.rb new file mode 100644 index 000000000..6b97ca919 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/property.rb @@ -0,0 +1,52 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::Property+ + # + # This entity represents a property object. + class Property + attr_accessor :id, :title, :description, :lat, :lng, :address, + :postal_code, :check_in_time, :check_out_time, :max_guests, + :surface, :bedroom_type_id, :property_type_id, :floor, + :images, :amenities, :owner_id, :security_deposit_type, + :security_deposit_amount + + attr_writer :active, :archived + + def initialize(id:, title:, description:, lat:, lng:, address:, + postal_code:, check_in_time:, check_out_time:, + max_guests:, surface:, bedroom_type_id:, property_type_id:, + floor:, images:, amenities:, active:, archived:, owner_id:, + security_deposit_type:, security_deposit_amount:) + @id = id + @title = title + @description = description + @lat = lat + @lng = lng + @address = address + @postal_code = postal_code + @check_in_time = check_in_time + @check_out_time = check_out_time + @max_guests = max_guests + @surface = surface + @bedroom_type_id = bedroom_type_id + @property_type_id = property_type_id + @owner_id = owner_id + @security_deposit_type = security_deposit_type + @security_deposit_amount = security_deposit_amount + @floor = floor + @active = active + @archived = archived + @images = images + @amenities = amenities + end + + def active? + @active + end + + def archived? + @archived + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/property_type.rb b/lib/concierge/suppliers/rentals_united/entities/property_type.rb new file mode 100644 index 000000000..787c7a9da --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/property_type.rb @@ -0,0 +1,24 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::PropertyType+ + # + # This entity represents a property type object. + # + # Attributes: + # + # +id+ - rentals united property type id + # +name+ - rentals united property type name + # +roomorama_name+ - roomorama name + # +roomorama_subtype_name+ - roomorama subtype name + class PropertyType + attr_reader :id, :name, :roomorama_name, :roomorama_subtype_name + + def initialize(id:, name:, roomorama_name:, roomorama_subtype_name:) + @id = id + @name = name + @roomorama_name = roomorama_name + @roomorama_subtype_name = roomorama_subtype_name + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/entities/season.rb b/lib/concierge/suppliers/rentals_united/entities/season.rb new file mode 100644 index 000000000..6ad37656a --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/entities/season.rb @@ -0,0 +1,29 @@ +module RentalsUnited + module Entities + # +RentalsUnited::Entities::Season+ + # + # This entity represents a rates season object. + class Season + attr_accessor :date_from, :date_to, :price + + def initialize(date_from:, date_to:, price:) + @date_from = date_from + @date_to = date_to + @price = price + end + + def has_price_for_date?(date) + date_range.include?(date) + end + + def number_of_days + date_range.count + end + + private + def date_range + date_from..date_to + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/importer.rb b/lib/concierge/suppliers/rentals_united/importer.rb new file mode 100644 index 000000000..daadca580 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/importer.rb @@ -0,0 +1,94 @@ +module RentalsUnited + # +RentalsUnited::Importer+ + # + # This class wraps supplier API and provides data for building properties. + # + # Usage + # + # importer = RentalsUnited::Importer.new(credentials) + class Importer + + attr_reader :credentials + + def initialize(credentials) + @credentials = credentials + end + + # Retrieves properties collection for a given owner by +owner_id+ + # + # Properties which are no longer available will be filtered out. + # + # Returns a +Result+ wrapping +Entities::PropertiesCollection+ object + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_properties_collection_for_owner(owner_id) + fetcher = Commands::PropertiesCollectionFetcher.new( + credentials, + owner_id + ) + fetcher.fetch_properties_collection_for_owner + end + + # Retrieves locations by given location_ids. + # + # Arguments: + # + # * +location_ids+ [Array] ids array of locations to fetch + # + # Returns a +Result+ wrapping +Array+ of +Entities::Location+ objects + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_locations(location_ids) + fetcher = Commands::LocationsFetcher.new(credentials, location_ids) + fetcher.fetch_locations + end + + # Retrieves locations - currencies mapping. + # + # Returns a +Result+ wrapping +Hash+ with location_id => currency pairs + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_location_currencies + fetcher = Commands::LocationCurrenciesFetcher.new(credentials) + fetcher.fetch_location_currencies + end + + # Retrieves property by its id. + # + # Returns a +Result+ wrapping +Entities::Property+ object + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_property(property_id) + property_fetcher = Commands::PropertyFetcher.new( + credentials, + property_id + ) + property_fetcher.fetch_property + end + + # Retrieves owner by id + # + # Returns a +Result+ wrapping +Entities::Owner+ object + # Returns a +Result+ with +Result::Error+ when operation fails + def fetch_owner(owner_id) + fetcher = Commands::OwnerFetcher.new(credentials, owner_id) + fetcher.fetch_owner + end + + # Retrieves availabilities for property by its id. + # + # Returns [Array] array with availabilities + def fetch_availabilities(property_id) + fetcher = Commands::AvailabilitiesFetcher.new( + credentials, + property_id + ) + + fetcher.fetch_availabilities + end + + # Retrieves season rates for property by its id. + # + # Returns [Array] array with season rate objects + def fetch_seasons(property_id) + fetcher = Commands::SeasonsFetcher.new(credentials, property_id) + fetcher.fetch_seasons + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/availability.rb b/lib/concierge/suppliers/rentals_united/mappers/availability.rb new file mode 100644 index 000000000..bb03db2dd --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/availability.rb @@ -0,0 +1,23 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Availability+ + # + # This class is responsible for building an availability object. + class Availability + attr_reader :hash + + def initialize(hash) + @hash = hash + end + + def build_availability + Entities::Availability.new( + date: Date.parse(hash["@Date"]), + available: hash["IsBlocked"] == false, + minimum_stay: hash["MinStay"].to_i, + changeover: hash["Changeover"].to_i + ) + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/calendar.rb b/lib/concierge/suppliers/rentals_united/mappers/calendar.rb new file mode 100644 index 000000000..a9786011c --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/calendar.rb @@ -0,0 +1,91 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Calendar+ + # + # This class is responsible for building a calendar for property. + class Calendar + attr_reader :property_id, :seasons, :availabilities + + SUPPORTED_CHANGEOVER_TYPE_IDS = [1, 2, 3, 4] + CHECK_IN_ALLOWED_CHANGEOVER_TYPE_IDS = [1, 4] + CHECK_OUT_ALLOWED_CHANGEOVER_TYPE_IDS = [2, 4] + + # Initialize +RentalsUnited::Mappers::Calendar+ + # + # Arguments + # + # * +propertyid+ [String] id of property + # * +seasons+ [Array] seasons + # * +availabilities+ [Array] availabilities + def initialize(property_id, seasons, availabilities) + @property_id = property_id + @seasons = seasons + @availabilities = availabilities + end + + # Builds calendar. + # + # Returns a +Result+ wrapping +Roomorama::Calendar+ + # Returns a +Result+ with +Result::Error+ when operation fails + def build_calendar + calendar = Roomorama::Calendar.new(property_id) + + entries_result = build_entries + return entries_result unless entries_result.success? + + entries = entries_result.value + entries.each { |entry| calendar.add(entry) } + + Result.new(calendar) + end + + private + def build_entries + entries = availabilities.map do |availability| + unless supported_changeover?(availability.changeover) + return Result.error(:not_supported_changeover) + end + + nightly_rate = rate_by_date(availability.date) + + if nightly_rate.zero? + available = false + checkin_allowed = false + checkout_allowed = false + else + available = availability.available + checkin_allowed = checkin_allowed?(availability.changeover) + checkout_allowed = checkout_allowed?(availability.changeover) + end + + Roomorama::Calendar::Entry.new( + date: availability.date.to_s, + available: available, + nightly_rate: nightly_rate, + minimum_stay: availability.minimum_stay, + checkin_allowed: checkin_allowed, + checkout_allowed: checkout_allowed + ) + end + Result.new(entries) + end + + def rate_by_date(date) + season = seasons.find { |s| s.has_price_for_date?(date) } + season&.price.to_f + end + + def supported_changeover?(changeover) + SUPPORTED_CHANGEOVER_TYPE_IDS.include?(changeover) + end + + def checkin_allowed?(changeover) + CHECK_IN_ALLOWED_CHANGEOVER_TYPE_IDS.include?(changeover) + end + + def checkout_allowed?(changeover) + CHECK_OUT_ALLOWED_CHANGEOVER_TYPE_IDS.include?(changeover) + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/image_set.rb b/lib/concierge/suppliers/rentals_united/mappers/image_set.rb new file mode 100644 index 000000000..3cb574b34 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/image_set.rb @@ -0,0 +1,53 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::ImageSet+ + # + # This class is responsible for building an array of images for + # properties and units. + # + # Array of images includes +Roomorama::Image+ objects + class ImageSet + attr_reader :raw_images + + # Rentals United image types mapping (image type id => name) + # Names are used as captions for +Roomorama::Image+ objects + IMAGE_TYPES = { + "1" => "Main image", + "2" => "Property plan", + "3" => "Interior", + "4" => "Exterior" + } + + # Initialize +RentalsUnited::Mappers::ImageSet+ mapper + # + # Arguments: + # + # * +raw_images+ [Array(Nori::StringWithAttributes)] array + def initialize(raw_images) + @raw_images = raw_images + end + + # Builds an array of property images + # + # If image URL is not valid (contains a space sign) then image is not + # included in the result array. + # + # Returns +Array+ array of images + def build_images + raw_images.map { |raw_image| build_image(raw_image) } + end + + private + def build_image(raw_image) + url = URI.encode(raw_image.to_s) + identifier = Digest::MD5.hexdigest(url) + + image = Roomorama::Image.new(identifier) + image.url = url + image.caption = IMAGE_TYPES[raw_image.attributes["ImageTypeID"]] + + image + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/location.rb b/lib/concierge/suppliers/rentals_united/mappers/location.rb new file mode 100644 index 000000000..1b33aefb9 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/location.rb @@ -0,0 +1,75 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Location+ + # + # This class is responsible for building a location object. + class Location + attr_reader :location_id, :raw_locations_database + + # Rentals United mapping of location ids and location types. + # Worldwide and Continent type locations are not mapped. + LOCATION_TYPES = { + 2 => :country, + 3 => :region, + 4 => :city, + 5 => :neighborhood + } + + # Initialize +RentalsUnited::Mappers::Location+ mapper + # + # Arguments: + # + # * +location_id+ [String] id of location + # * +raw_locations_database+ [Array] database of locations data + def initialize(location_id, raw_locations_database) + @location_id = location_id + @raw_locations_database = raw_locations_database + end + + # Iterate over location hierarchy by location type id attribute. + # + # Each level of hierarchy provide location data: region, city, country + # names. + # + # Iteration starts from the level of the current type of given location + # and ends when hits "Country" type. + def build_location + location = Entities::Location.new(location_id) + + current_level = find_location_data(location_id) + return nil unless current_level + + current_level_type = current_level[:type] + + location_hash = {} + update_location_hash(location_hash, current_level) + + while(LOCATION_TYPES.keys.include?(current_level_type)) do + parent_location_id = current_level[:parent_id] + + current_level = find_location_data(parent_location_id) + return nil unless current_level + + current_level_type = current_level[:type] + + update_location_hash(location_hash, current_level) + end + + location.load(location_hash) + end + + private + def find_location_data(id) + raw_locations_database.find do |location| + location[:id] == id + end + end + + def update_location_hash(location_hash, current_level) + key = LOCATION_TYPES[current_level[:type]] + value = current_level[:name] + location_hash[key] = value + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/owner.rb b/lib/concierge/suppliers/rentals_united/mappers/owner.rb new file mode 100644 index 000000000..2f177d33e --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/owner.rb @@ -0,0 +1,29 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Owner+ + # + # This class is responsible for building an owner object. + class Owner + attr_reader :owner_hash + + # Initialize +RentalsUnited::Mappers::Owner+ mapper + # + # Arguments: + # + # * +owner_hash+ [Concierge::SafeAccessHash] owner hash + def initialize(owner_hash) + @owner_hash = owner_hash + end + + def build_owner + Entities::Owner.new( + id: owner_hash.get("@OwnerID"), + first_name: owner_hash.get("FirstName"), + last_name: owner_hash.get("SurName"), + email: owner_hash.get("Email"), + phone: owner_hash.get("Phone") + ) + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/price.rb b/lib/concierge/suppliers/rentals_united/mappers/price.rb new file mode 100644 index 000000000..b3ac0aed9 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/price.rb @@ -0,0 +1,29 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Price+ + # + # This class is responsible for building a +Entities::Price+ object + class Price + attr_reader :price + + # Initialize Price mapper + # + # Arguments: + # + # * +price+ [String] price + def initialize(price) + @price = price.to_s + end + + # Builds price + # + # Returns [Entities::Price] + def build_price + Entities::Price.new( + total: price.to_f, + available: price.empty? ? false : true + ) + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/properties_collection.rb b/lib/concierge/suppliers/rentals_united/mappers/properties_collection.rb new file mode 100644 index 000000000..96c0aae97 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/properties_collection.rb @@ -0,0 +1,36 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::propertiescollection+ + # + # This class is responsible for building a properties collection object + class PropertiesCollection + attr_reader :properties + + # Initialize +RentalsUnited::Mappers::PropertiesCollection+ mapper + # + # Arguments: + # + # * +properties+ [Array] array with property collection hashes + def initialize(properties) + @properties = properties + end + + def build_properties_collection + entries = build_entries + + Entities::PropertiesCollection.new(entries) + end + + private + def build_entries + properties.map do |hash| + safe_hash = Concierge::SafeAccessHash.new(hash) + { + property_id: safe_hash.get("ID"), + location_id: safe_hash.get("DetailedLocationID") + } + end + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/property.rb b/lib/concierge/suppliers/rentals_united/mappers/property.rb new file mode 100644 index 000000000..68262e78e --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/property.rb @@ -0,0 +1,106 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Property+ + # + # This class is responsible for building a + # +RentalsUnited::Entities::Property+ object from a hash which was fetched + # from the RentalsUnited API. + class Property + attr_reader :property_hash + + EN_DESCRIPTION_LANG_CODE = "1" + + # Initialize +RentalsUnited::Mappers::Property+ + # + # Arguments: + # + # * +property_hash+ [Concierge::SafeAccessHash] property hash object + def initialize(property_hash) + @property_hash = property_hash + end + + # Builds a property + # + # Returns [RentalsUnited::Entities::Property] + def build_property + property = Entities::Property.new( + id: property_hash.get("ID"), + title: property_hash.get("Name"), + lat: property_hash.get("Coordinates.Latitude").to_f, + lng: property_hash.get("Coordinates.Longitude").to_f, + address: property_hash.get("Street"), + postal_code: property_hash.get("ZipCode").to_s.strip, + max_guests: property_hash.get("CanSleepMax").to_i, + bedroom_type_id: property_hash.get("PropertyTypeID"), + property_type_id: property_hash.get("ObjectTypeID"), + active: property_hash.get("IsActive"), + archived: property_hash.get("IsArchived"), + surface: property_hash.get("Space").to_i, + owner_id: property_hash.get("OwnerID"), + security_deposit_amount: property_hash.get("SecurityDeposit").to_f, + security_deposit_type: security_deposit_type, + check_in_time: check_in_time, + check_out_time: check_out_time, + floor: floor, + description: en_description(property_hash), + images: build_images, + amenities: build_amenities + ) + + property + end + + private + def build_amenities + Array(property_hash.get("Amenities.Amenity")) + end + + def build_images + raw_images = Array(property_hash.get("Images.Image")) + + mapper = Mappers::ImageSet.new(raw_images) + mapper.build_images + end + + # RU sends -1000 for Basement + # 0 for Ground + # 0..100 for usual floor number + # + # Replace -1000 with just -1 because we don't want our users to + # be burnt away -1000 floors under the earth. + def floor + ru_floor_value = property_hash.get("Floor").to_i + return -1 if ru_floor_value == -1000 + return ru_floor_value + end + + def en_description(hash) + descriptions = hash.get("Descriptions.Description") + en_description = Array(descriptions).find do |desc| + desc["@LanguageID"] == EN_DESCRIPTION_LANG_CODE + end + + en_description["Text"] if en_description + end + + def security_deposit_type + security_deposit = property_hash.get("SecurityDeposit") + + if security_deposit + security_deposit.attributes["DepositTypeID"] + end + end + + def check_in_time + from = property_hash.get("CheckInOut.CheckInFrom") + to = property_hash.get("CheckInOut.CheckInTo") + + "#{from}-#{to}" if from && to + end + + def check_out_time + property_hash.get("CheckInOut.CheckOutUntil") + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/quotation.rb b/lib/concierge/suppliers/rentals_united/mappers/quotation.rb new file mode 100644 index 000000000..5db2a0ff1 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/quotation.rb @@ -0,0 +1,38 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Quotation+ + # + # This class is responsible for building a +Quotation+ object + class Quotation + attr_reader :price, :currency, :quotation_params + + # Initialize Quotation mapper + # + # Arguments: + # + # * +price+ [Entities::Price] price + # * +currency+ [String] currency code + # * +quotation_params+ [Concierge::SafeAccessHash] quotation parameters + def initialize(price, currency, quotation_params) + @price = price + @currency = currency + @quotation_params = quotation_params + end + + # Builds quotation + # + # Returns [Quotation] + def build_quotation + ::Quotation.new( + property_id: quotation_params[:property_id], + check_in: quotation_params[:check_in].to_s, + check_out: quotation_params[:check_out].to_s, + guests: quotation_params[:guests], + total: price.total, + available: price.available?, + currency: currency + ) + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/reservation.rb b/lib/concierge/suppliers/rentals_united/mappers/reservation.rb new file mode 100644 index 000000000..93e18c301 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/reservation.rb @@ -0,0 +1,32 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::Reservation+ + # + # This class is responsible for building a +Reservation+ object + class Reservation + attr_reader :reservation_code, :reservation_params + + # Initialize Reservation mapper + # + # Arguments: + # + # * +reservation_code+ [String] reservation code + # * +reservation_params+ [Concierge::SafeAccessHash] parameters + def initialize(reservation_code, reservation_params) + @reservation_code = reservation_code + @reservation_params = reservation_params + end + + # Builds reservation + # + # Returns [Reservation] + def build_reservation + ::Reservation.new( + reservation_params.to_h.merge!( + reference_number: reservation_code + ) + ) + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/mappers/roomorama_property.rb b/lib/concierge/suppliers/rentals_united/mappers/roomorama_property.rb new file mode 100644 index 000000000..c3e235bbe --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/mappers/roomorama_property.rb @@ -0,0 +1,193 @@ +module RentalsUnited + module Mappers + # +RentalsUnited::Mappers::RoomoramaProperty+ + # + # This class is responsible for building a +Roomorama::Property+ object + class RoomoramaProperty + attr_reader :ru_property, :location, :owner, :seasons + + EN_DESCRIPTION_LANG_CODE = "1" + CANCELLATION_POLICY = "super_elite" + DEFAULT_PROPERTY_RATE = "9999" + MINIMUM_STAY = 1 + SURFACE_UNIT = "metric" + + # List of supported by Roomorama security deposit types + NO_DEPOSIT_ID = "1" + FLAT_AMOUNT_PER_STAY_ID = "5" + SUPPORTED_SECURITY_DEPOSIT_TYPES = [ + NO_DEPOSIT_ID, + FLAT_AMOUNT_PER_STAY_ID + ] + SECURITY_DEPOSIT_PAYMENT_TYPE = 'cash' + + # Initialize +RentalsUnited::Mappers::Property+ + # + # Arguments: + # + # * +ru_property+ [Entities::Property] RU property object + # * +location+ [Entities::Location] location object + # * +owner+ [Entities::OwnerID] owner object + # * +seasons+ [Array"200.0000", + # "Extra"=>"10.0000", + # "@DateFrom"=>"2016-09-07", + # "@DateTo"=>"2016-09-30" + # }) + def initialize(hash) + @hash = hash + end + + def build_season + Entities::Season.new( + date_from: Date.parse(hash["@DateFrom"]), + date_to: Date.parse(hash["@DateTo"]), + price: hash["Price"].to_f + ) + end + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/payload_builder.rb b/lib/concierge/suppliers/rentals_united/payload_builder.rb new file mode 100644 index 000000000..3a965d3ce --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/payload_builder.rb @@ -0,0 +1,113 @@ +require 'tilt' + +module RentalsUnited + # +RentalsUnited::PayloadBuilder+ + # + # This class builds XML payloads for all RentalsUnited endpoints. + class PayloadBuilder + TEMPLATES_PATH = "lib/concierge/suppliers/rentals_united/templates" + + attr_reader :credentials + + def initialize(credentials) + @credentials = credentials + end + + def build_properties_collection_fetch_payload(owner_id) + template_locals = { + credentials: credentials, + owner_id: owner_id + } + render(:properties_collection_fetch, template_locals) + end + + def build_locations_fetch_payload + template_locals = { credentials: credentials } + render(:locations_fetch, template_locals) + end + + def build_location_currencies_fetch_payload + template_locals = { credentials: credentials } + render(:location_currencies_fetch, template_locals) + end + + def build_property_fetch_payload(property_id) + template_locals = { + credentials: credentials, + property_id: property_id + } + render(:property_fetch, template_locals) + end + + def build_owner_fetch_payload(owner_id) + template_locals = { + credentials: credentials, + owner_id: owner_id + } + render(:owner_fetch, template_locals) + end + + def build_availabilities_fetch_payload(property_id, date_from, date_to) + template_locals = { + credentials: credentials, + property_id: property_id, + date_from: date_from, + date_to: date_to + } + render(:availabilities_fetch, template_locals) + end + + def build_seasons_fetch_payload(property_id, date_from, date_to) + template_locals = { + credentials: credentials, + property_id: property_id, + date_from: date_from, + date_to: date_to + } + render(:seasons_fetch, template_locals) + end + + def build_price_fetch_payload(property_id:, check_in:, check_out:, num_guests:) + template_locals = { + credentials: credentials, + property_id: property_id, + check_in: check_in, + check_out: check_out, + num_guests: num_guests + } + render(:price_fetch, template_locals) + end + + def build_booking_payload(property_id:, check_in:, check_out:, num_guests:, total:, user:) + template_locals = { + credentials: credentials, + property_id: property_id, + num_guests: num_guests, + check_in: check_in, + check_out: check_out, + total: total, + first_name: user.fetch(:first_name), + last_name: user.fetch(:last_name), + email: user.fetch(:email), + phone: user.fetch(:phone), + address: user.fetch(:address), + postal_code: user.fetch(:postal_code) + } + render(:booking, template_locals) + end + + def build_cancel_payload(reference_number) + template_locals = { + credentials: credentials, + reference_number: reference_number + } + render(:cancel, template_locals) + end + + private + def render(template_name, local_vars) + path = Hanami.root.join(TEMPLATES_PATH, "#{template_name}.xml.erb") + Tilt.new(path).render(Object.new, local_vars) + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/response_parser.rb b/lib/concierge/suppliers/rentals_united/response_parser.rb new file mode 100644 index 000000000..97844860d --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/response_parser.rb @@ -0,0 +1,30 @@ +module RentalsUnited + # +RentalsUnited::ResponseParser+ + # + # This class is intended to convert all response data from RentalsUnited API + # to a hash. + # +Concierge::SafeAccessHash+ is used as hash implemetation to provide + # safe access to hash keys and values + class ResponseParser + # Convert +Result+ response to a safe hash. + # +Nori+ library is used as XML to Hash translator + # + # Example: + # + # hash = ResponseParser.to_hash(http_response) + # + # Returns +Concierge::SafeAccessHash+ object + def to_hash(result_body) + safe_hash(parse_hash(result_body)) + end + + private + def safe_hash(usual_hash) + Concierge::SafeAccessHash.new(usual_hash) + end + + def parse_hash(response) + Nori.new.parse(response) + end + end +end diff --git a/lib/concierge/suppliers/rentals_united/templates/availabilities_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/availabilities_fetch.xml.erb new file mode 100644 index 000000000..0c00b7c35 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/availabilities_fetch.xml.erb @@ -0,0 +1,9 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + <%= property_id %> + <%= date_from %> + <%= date_to %> + diff --git a/lib/concierge/suppliers/rentals_united/templates/booking.xml.erb b/lib/concierge/suppliers/rentals_united/templates/booking.xml.erb new file mode 100644 index 000000000..f2c361b5d --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/booking.xml.erb @@ -0,0 +1,30 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + + + + <%= property_id %> + <%= check_in %> + <%= check_out %> + <%= num_guests %> + + <%= total %> + <%= total %> + <%= total %> + + + + + <%= first_name %> + <%= last_name %> + <%= email %> + <%= phone %> +
<%= address %>
+ <%= postal_code %> +
+ +
+
diff --git a/lib/concierge/suppliers/rentals_united/templates/cancel.xml.erb b/lib/concierge/suppliers/rentals_united/templates/cancel.xml.erb new file mode 100644 index 000000000..a010ff9e6 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/cancel.xml.erb @@ -0,0 +1,7 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + <%= reference_number %> + diff --git a/lib/concierge/suppliers/rentals_united/templates/location_currencies_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/location_currencies_fetch.xml.erb new file mode 100644 index 000000000..dfd13dcbf --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/location_currencies_fetch.xml.erb @@ -0,0 +1,6 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + diff --git a/lib/concierge/suppliers/rentals_united/templates/locations_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/locations_fetch.xml.erb new file mode 100644 index 000000000..44125597d --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/locations_fetch.xml.erb @@ -0,0 +1,6 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + diff --git a/lib/concierge/suppliers/rentals_united/templates/owner_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/owner_fetch.xml.erb new file mode 100644 index 000000000..77a321414 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/owner_fetch.xml.erb @@ -0,0 +1,7 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + <%= owner_id %> + diff --git a/lib/concierge/suppliers/rentals_united/templates/price_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/price_fetch.xml.erb new file mode 100644 index 000000000..2a6af3665 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/price_fetch.xml.erb @@ -0,0 +1,10 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + <%= property_id %> + <%= check_in %> + <%= check_out %> + <%= num_guests %> + diff --git a/lib/concierge/suppliers/rentals_united/templates/properties_collection_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/properties_collection_fetch.xml.erb new file mode 100644 index 000000000..84d40b8d9 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/properties_collection_fetch.xml.erb @@ -0,0 +1,8 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + <%= owner_id %> + false + diff --git a/lib/concierge/suppliers/rentals_united/templates/property_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/property_fetch.xml.erb new file mode 100644 index 000000000..b55f264c9 --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/property_fetch.xml.erb @@ -0,0 +1,8 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + + <%= property_id %> + diff --git a/lib/concierge/suppliers/rentals_united/templates/seasons_fetch.xml.erb b/lib/concierge/suppliers/rentals_united/templates/seasons_fetch.xml.erb new file mode 100644 index 000000000..acd987f6f --- /dev/null +++ b/lib/concierge/suppliers/rentals_united/templates/seasons_fetch.xml.erb @@ -0,0 +1,9 @@ + + + <%= credentials.username %> + <%= credentials.password %> + + <%= property_id %> + <%= date_from %> + <%= date_to %> + diff --git a/lib/concierge/suppliers/waytostay/quote.rb b/lib/concierge/suppliers/waytostay/quote.rb index 3295f3a09..da7c4f980 100644 --- a/lib/concierge/suppliers/waytostay/quote.rb +++ b/lib/concierge/suppliers/waytostay/quote.rb @@ -87,21 +87,10 @@ def quote_params_from(response) check_out: response.get("booking_details.departure_date"), guests: response.get("booking_details.number_of_adults"), total: response.get("pricing.pricing_summary.gross_total"), - host_fee_percentage: host.fee_percentage, currency: response.get("pricing.currency"), available: true } end - # Get the first host under Waytostay, because there - # should only be one host - # - def host - @host ||= begin - supplier = SupplierRepository.named(Waytostay::Client::SUPPLIER_NAME) - HostRepository.from_supplier(supplier).first - end - end - end end diff --git a/lib/concierge/version.rb b/lib/concierge/version.rb index a44489ec6..f199cb0d4 100644 --- a/lib/concierge/version.rb +++ b/lib/concierge/version.rb @@ -1,3 +1,3 @@ module Concierge - VERSION = "0.11.6" + VERSION = "0.12.0" end diff --git a/spec/api/controllers/atleisure/quote_spec.rb b/spec/api/controllers/atleisure/quote_spec.rb index d2dcec5ce..cc56dcff8 100644 --- a/spec/api/controllers/atleisure/quote_spec.rb +++ b/spec/api/controllers/atleisure/quote_spec.rb @@ -7,13 +7,12 @@ include Support::Fixtures include Support::Factories - before do - supplier = create_supplier(name: AtLeisure::Client::SUPPLIER_NAME) - create_host(supplier_id: supplier.id, fee_percentage: 7.0) - end + let(:supplier) { create_supplier(name: AtLeisure::Client::SUPPLIER_NAME) } + let(:host) { create_host(supplier_id: supplier.id, fee_percentage: 7.0) } + let!(:property) { create_property(identifier: "AT-123", host_id: host.id) } let(:params) { - { property_id: "AT-123", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } + { property_id: property.identifier, check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } } let(:endpoint) { AtLeisure::Price::ENDPOINT } diff --git a/spec/api/controllers/ciirus/quote_spec.rb b/spec/api/controllers/ciirus/quote_spec.rb index eecbb6433..921827a45 100644 --- a/spec/api/controllers/ciirus/quote_spec.rb +++ b/spec/api/controllers/ciirus/quote_spec.rb @@ -8,10 +8,11 @@ include Support::SOAPStubbing include Support::Factories - let!(:host) { create_host(fee_percentage: 7) } + let!(:supplier) { create_supplier(name: Ciirus::Client::SUPPLIER_NAME) } + let!(:host) { create_host(fee_percentage: 7, supplier_id: supplier.id) } let!(:property) { create_property(identifier: '38180', host_id: host.id) } let(:params) { - { property_id: '38180', check_in: '2016-05-01', check_out: '2016-05-12', guests: 3 } + { property_id: property.identifier, check_in: '2016-05-01', check_out: '2016-05-12', guests: 3 } } let(:success_response) { read_fixture('ciirus/responses/property_quote_response.xml') } @@ -84,4 +85,4 @@ def provoke_failure! end end end -end \ No newline at end of file +end diff --git a/spec/api/controllers/jtb/quote_spec.rb b/spec/api/controllers/jtb/quote_spec.rb index 8c9db8191..4b6287510 100644 --- a/spec/api/controllers/jtb/quote_spec.rb +++ b/spec/api/controllers/jtb/quote_spec.rb @@ -5,12 +5,17 @@ RSpec.describe API::Controllers::JTB::Quote do include Support::HTTPStubbing include Support::Fixtures + include Support::Factories + + let(:supplier) { create_supplier(name: JTB::Client::SUPPLIER_NAME) } + let(:host) { create_host(supplier_id: supplier.id) } + let(:property) { create_property(identifier: "J123", host_id: host.id) } it_behaves_like "performing multi unit parameter validations", controller_generator: -> { described_class.new } it_behaves_like "external error reporting" do let(:params) { - { property_id: "321", unit_id: "123", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } + { property_id: property.identifier, unit_id: "123", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } } let(:supplier_name) { "JTB" } let(:error_code) { "savon_erorr" } @@ -23,7 +28,7 @@ def provoke_failure! describe "#call" do let(:params) { - { property_id: "J123", unit_id: "123J", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } + { property_id: property.identifier, unit_id: "123J", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } } it "indicates the unit is unavailable in case there are no rate plans" do @@ -44,7 +49,7 @@ def provoke_failure! context "when stay length is > 15 days" do let(:params) { - { property_id: "J123", unit_id: "123J", check_in: "2016-02-22", check_out: "2016-03-25", guests: 2 } + { property_id: property.identifier, unit_id: "123J", check_in: "2016-02-22", check_out: "2016-03-25", guests: 2 } } it "respond with the stay_too_long error" do response = parse_response(subject.call(params)) diff --git a/spec/api/controllers/kigo/legacy/quote_spec.rb b/spec/api/controllers/kigo/legacy/quote_spec.rb index 309c114d9..a3bf236e3 100644 --- a/spec/api/controllers/kigo/legacy/quote_spec.rb +++ b/spec/api/controllers/kigo/legacy/quote_spec.rb @@ -8,8 +8,9 @@ include Support::Fixtures include Support::Factories - let!(:host) { create_host(fee_percentage: 7.0) } - let!(:property) { create_property(identifier: "567") } + let(:supplier) { create_supplier(name: Kigo::Legacy::SUPPLIER_NAME) } + let(:host) { create_host(supplier_id: supplier.id, fee_percentage: 7.0) } + let(:property) { create_property(identifier: "567", host_id: host.id) } let(:params) { { property_id: property.identifier, check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } diff --git a/spec/api/controllers/kigo/quote_spec.rb b/spec/api/controllers/kigo/quote_spec.rb index be00828c0..c597d0082 100644 --- a/spec/api/controllers/kigo/quote_spec.rb +++ b/spec/api/controllers/kigo/quote_spec.rb @@ -8,8 +8,9 @@ include Support::Fixtures include Support::Factories - let!(:host) { create_host(fee_percentage: 7.0) } - let!(:property) { create_property(identifier: "567") } + let!(:supplier) { create_supplier(name: Kigo::Client::SUPPLIER_NAME) } + let!(:host) { create_host(fee_percentage: 7.0, supplier_id: supplier.id) } + let!(:property) { create_property(identifier: "567", host_id: host.id) } let(:params) { { property_id: property.identifier, check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } diff --git a/spec/api/controllers/poplidays/quote_spec.rb b/spec/api/controllers/poplidays/quote_spec.rb index abf72a7b1..13d2739de 100644 --- a/spec/api/controllers/poplidays/quote_spec.rb +++ b/spec/api/controllers/poplidays/quote_spec.rb @@ -10,8 +10,9 @@ let!(:supplier) { create_supplier(name: Poplidays::Client::SUPPLIER_NAME) } let!(:host) { create_host(supplier_id: supplier.id, fee_percentage: 5) } + let!(:property) { create_property(identifier: '48327', host_id: host.id) } let(:params) { - { property_id: '48327', check_in: '2016-12-17', check_out: '2016-12-26', guests: 2 } + { property_id: property.identifier, check_in: '2016-12-17', check_out: '2016-12-26', guests: 2 } } let(:credentials) do double(url: 'api.poplidays.com', diff --git a/spec/api/controllers/rentals_united/booking_spec.rb b/spec/api/controllers/rentals_united/booking_spec.rb new file mode 100644 index 000000000..d0604fc72 --- /dev/null +++ b/spec/api/controllers/rentals_united/booking_spec.rb @@ -0,0 +1,57 @@ +require "spec_helper" +require_relative "../shared/booking_validations" + +RSpec.describe API::Controllers::RentalsUnited::Booking do + include Support::HTTPStubbing + include Support::Fixtures + + let(:params) do + { + property_id: '588999', + check_in: '2016-02-02', + check_out: '2016-02-03', + guests: 1, + currency_code: 'EUR', + subtotal: '123.45', + customer: { + first_name: 'Test', + last_name: 'User', + email: 'testuser@example.com' + } + } + end + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:safe_params) { Concierge::SafeAccessHash.new(params) } + let(:controller) { described_class.new } + + it_behaves_like "performing booking parameters validations", controller_generator: -> { described_class.new } + + it "returns success response if booking request is completed successfully" do + stub_data = read_fixture("rentals_united/reservations/success.xml") + stub_call(:post, credentials.url) { [200, {}, stub_data] } + + response = parse_response(controller.call(params)) + expect(response.status).to eq 200 + expect(response.body['status']).to eq('ok') + expect(response.body['reference_number']).to eq('90377000') + expect(response.body['property_id']).to eq('588999') + expect(response.body['check_in']).to eq('2016-02-02') + expect(response.body['check_out']).to eq('2016-02-03') + expect(response.body['guests']).to eq(1) + expect(response.body['customer']).to eq(params[:customer]) + end + + it "returns error response if booking request failed" do + stub_data = read_fixture("rentals_united/reservations/not_available.xml") + stub_call(:post, credentials.url) { [200, {}, stub_data] } + + response = parse_response(controller.call(params)) + expect(response.status).to eq 503 + expect(response.body['status']).to eq('error') + expect(response["body"]["errors"]["booking"]).to eq( + "Could not create booking with remote supplier" + ) + end +end diff --git a/spec/api/controllers/rentals_united/cancel_spec.rb b/spec/api/controllers/rentals_united/cancel_spec.rb new file mode 100644 index 000000000..9ed58a47f --- /dev/null +++ b/spec/api/controllers/rentals_united/cancel_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' +require_relative "../shared/cancel" + +RSpec.describe API::Controllers::RentalsUnited::Cancel do + it_behaves_like "cancel action" do + let(:success_cases) { + [ + { params: {reference_number: "A023", inquiry_id: "303"}, cancelled_reference_number: "XYZ" }, + { params: {reference_number: "A024", inquiry_id: "308"}, cancelled_reference_number: "ASD" }, + ] + } + let(:error_cases) { + [ + { params: {reference_number: "A123", inquiry_id: "392"}, error: {"cancellation" => "Could not cancel with remote supplier"} }, + { params: {reference_number: "A124", inquiry_id: "399"}, error: {"cancellation" => "Already cancelled"} }, + ] + } + + before do + allow_any_instance_of(RentalsUnited::Client).to receive(:cancel) do |instance, par| + result = nil + error_cases.each do |kase| + if par.reference_number == kase[:params][:reference_number] + result = Result.error(:already_cancelled, kase[:error]) + break + end + end + success_cases.each do |kase| + if par.reference_number == kase[:params][:reference_number] + result = Result.new(kase[:cancelled_reference_number]) + break + end + end + result + end + end + end +end diff --git a/spec/api/controllers/rentals_united/quote_spec.rb b/spec/api/controllers/rentals_united/quote_spec.rb new file mode 100644 index 000000000..be91d2b0b --- /dev/null +++ b/spec/api/controllers/rentals_united/quote_spec.rb @@ -0,0 +1,96 @@ +require "spec_helper" +require_relative "../shared/quote_validations" +require_relative "../shared/external_error_reporting" + +RSpec.describe API::Controllers::RentalsUnited::Quote do + include Support::HTTPStubbing + include Support::Fixtures + include Support::Factories + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + + before do + supplier = create_supplier(name: supplier_name) + host = create_host(identifier: "ru-host", supplier_id: supplier.id) + create_property( + identifier: "321", + host_id: host.id, + data: { :currency => "USD" } + ) + end + + let(:params) do + { + property_id: "321", + check_in: "2016-03-22", + check_out: "2016-03-25", + guests: 2 + } + end + + it_behaves_like "performing parameter validations", controller_generator: -> { described_class.new } do + let(:valid_params) { params } + end + + it_behaves_like "external error reporting" do + def provoke_failure! + stub_call(:post, credentials.url) do + raise Faraday::TimeoutError + end + Struct.new(:code).new("connection_timeout") + end + end + + describe "#call" do + context "when params are valid" do + let(:params) do + { + property_id: "321", + check_in: "2016-03-22", + check_out: "2016-03-25", + guests: 2 + } + end + + it "respond with successfull response" do + stub_data = read_fixture("rentals_united/quotations/success.xml") + stub_call(:post, credentials.url) { [200, {}, stub_data] } + + response = parse_response(subject.call(params)) + expect(response.status).to eq 200 + expect(response.body['status']).to eq("ok") + expect(response.body['available']).to be true + expect(response.body['property_id']).to eq("321") + expect(response.body['check_in']).to eq("2016-03-22") + expect(response.body['check_out']).to eq("2016-03-25") + expect(response.body['guests']).to eq(2) + expect(response.body['currency']).to eq("USD") + expect(response.body['total']).to eq(284.5) + expect(response.body['net_rate']).to eq(284.5) + expect(response.body['host_fee']).to eq(0.0) + expect(response.body['host_fee_percentage']).to eq(0.0) + end + + it "respond with successfull response but unavailable quotation" do + stub_data = read_fixture("rentals_united/quotations/not_available.xml") + stub_call(:post, credentials.url) { [200, {}, stub_data] } + + response = parse_response(subject.call(params)) + expect(response.status).to eq 200 + expect(response.body['status']).to eq("ok") + expect(response.body['available']).to be false + expect(response.body['property_id']).to eq("321") + expect(response.body['check_in']).to eq("2016-03-22") + expect(response.body['check_out']).to eq("2016-03-25") + expect(response.body['guests']).to eq(2) + expect(response.body['currency']).to eq(nil) + expect(response.body['total']).to eq(nil) + expect(response.body['net_rate']).to eq(nil) + expect(response.body['host_fee']).to eq(nil) + expect(response.body['host_fee_percentage']).to eq(nil) + end + end + end +end + diff --git a/spec/api/controllers/saw/quote_spec.rb b/spec/api/controllers/saw/quote_spec.rb index b6cb37812..e9195f921 100644 --- a/spec/api/controllers/saw/quote_spec.rb +++ b/spec/api/controllers/saw/quote_spec.rb @@ -5,11 +5,16 @@ RSpec.describe API::Controllers::SAW::Quote do include Support::HTTPStubbing include Support::Fixtures + include Support::Factories include Support::SAW::MockRequest + let(:supplier) { create_supplier(name: SAW::Client::SUPPLIER_NAME) } + let(:host) { create_host(fee_percentage: 7.0, supplier_id: supplier.id) } + let(:property) { create_property(identifier: "567", host_id: host.id) } + let(:params) do { - property_id: "1", + property_id: property.identifier, unit_id: '10612', check_in: "2015-02-26", check_out: "2015-02-28", diff --git a/spec/api/controllers/shared/kigo_price_quotation.rb b/spec/api/controllers/shared/kigo_price_quotation.rb index 1b36a9c1f..80e9c5536 100644 --- a/spec/api/controllers/shared/kigo_price_quotation.rb +++ b/spec/api/controllers/shared/kigo_price_quotation.rb @@ -68,7 +68,6 @@ before { stub_call(:post, endpoint) { [200, {}, read_fixture("kigo/success.json")] } } it "returns available quotations with price when the call is successful" do - allow_any_instance_of(Kigo::ResponseParser).to receive(:host) { Host.new(fee_percentage: 0) } response = parse_response(described_class.new.call(params)) expect(response.status).to eq 200 @@ -83,7 +82,6 @@ end it "returns available quotations with gross rate" do - allow_any_instance_of(Kigo::ResponseParser).to receive(:host) { Host.new(fee_percentage: 7.0) } response = parse_response(described_class.new.call(params)) expect(response.status).to eq 200 diff --git a/spec/api/controllers/shared/multi_unit_quote_validations.rb b/spec/api/controllers/shared/multi_unit_quote_validations.rb index 51d0d24ce..398504363 100644 --- a/spec/api/controllers/shared/multi_unit_quote_validations.rb +++ b/spec/api/controllers/shared/multi_unit_quote_validations.rb @@ -4,7 +4,7 @@ RSpec.shared_examples "performing multi unit parameter validations" do |controller_generator:| let(:params) { - { property_id: "A123", check_in: "2016-03-22", check_out: "2016-03-24", guests: 2, unit_id: "EX333" } + { property_id: property.identifier, check_in: "2016-03-22", check_out: "2016-03-24", guests: 2, unit_id: "EX333" } } it_behaves_like "performing parameter validations", controller_generator: -> { described_class.new } do diff --git a/spec/api/controllers/shared/quote_validations.rb b/spec/api/controllers/shared/quote_validations.rb index 26303d336..7a77c11fa 100644 --- a/spec/api/controllers/shared/quote_validations.rb +++ b/spec/api/controllers/shared/quote_validations.rb @@ -84,6 +84,16 @@ expect(response.body["errors"]).to eq({ "quote" => "Could not quote price with remote supplier" }) end + it "returns 404 if property is not found in the database" do + controller = controller_generator.call + allow(controller).to receive(:property_exists?) { false } + + response = call(controller, valid_params) + expect(response.status).to eq 404 + expect(response.body["status"]).to eq "error" + expect(response.body["errors"]).to eq("Property not found") + end + private def call(controller, params) diff --git a/spec/api/controllers/waytostay/quote_spec.rb b/spec/api/controllers/waytostay/quote_spec.rb index 160d34c89..419a169b1 100644 --- a/spec/api/controllers/waytostay/quote_spec.rb +++ b/spec/api/controllers/waytostay/quote_spec.rb @@ -5,9 +5,13 @@ RSpec.describe API::Controllers::Waytostay::Quote do include Support::HTTPStubbing + include Support::Factories + let(:supplier) { create_supplier(name: Waytostay::Client::SUPPLIER_NAME) } + let(:host) { create_host(supplier_id: supplier.id, fee_percentage: 7) } + let(:property) { create_property(identifier: "567", host_id: host.id) } let(:params) { - { property_id: "567", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } + { property_id: property.identifier, check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } } it_behaves_like "performing parameter validations", controller_generator: -> { described_class.new } do @@ -16,7 +20,7 @@ it_behaves_like "external error reporting" do let(:params) { - { property_id: "321", unit_id: "123", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } + { property_id: property.identifier, unit_id: "123", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } } let(:supplier_name) { "WayToStay" } let(:error_code) { "savon_erorr" } diff --git a/spec/fixtures/rentals_united/availabilities/not_found.xml b/spec/fixtures/rentals_united/availabilities/not_found.xml new file mode 100644 index 000000000..323837b71 --- /dev/null +++ b/spec/fixtures/rentals_united/availabilities/not_found.xml @@ -0,0 +1,3 @@ + + Property does not exist. + diff --git a/spec/fixtures/rentals_united/availabilities/success.xml b/spec/fixtures/rentals_united/availabilities/success.xml new file mode 100644 index 000000000..31974d28b --- /dev/null +++ b/spec/fixtures/rentals_united/availabilities/success.xml @@ -0,0 +1,125 @@ + + Success + + + true + 1 + 4 + + + false + 2 + 4 + + + false + 1 + 4 + + + true + 1 + 4 + + + false + 1 + 4 + + + false + 1 + 4 + + + false + 1 + 4 + + + true + 1 + 4 + + + true + 1 + 4 + + + false + 1 + 4 + + + false + 1 + 4 + + + true + 1 + 4 + + + false + 1 + 4 + + + false + 1 + 4 + + + false + 1 + 4 + + + false + 1 + 4 + + + true + 1 + 4 + + + true + 1 + 4 + + + true + 1 + 4 + + + true + 1 + 4 + + + true + 1 + 4 + + + true + 1 + 4 + + + false + 1 + 4 + + + true + 1 + 4 + + + diff --git a/spec/fixtures/rentals_united/bad_xml.xml b/spec/fixtures/rentals_united/bad_xml.xml new file mode 100644 index 000000000..2d812ae30 --- /dev/null +++ b/spec/fixtures/rentals_united/bad_xml.xml @@ -0,0 +1 @@ +Bad xml diff --git a/spec/fixtures/rentals_united/cancel/does_not_exist.xml b/spec/fixtures/rentals_united/cancel/does_not_exist.xml new file mode 100644 index 000000000..fabbbc663 --- /dev/null +++ b/spec/fixtures/rentals_united/cancel/does_not_exist.xml @@ -0,0 +1,3 @@ + + Reservation does not exist. + diff --git a/spec/fixtures/rentals_united/cancel/success.xml b/spec/fixtures/rentals_united/cancel/success.xml new file mode 100644 index 000000000..1d52971d7 --- /dev/null +++ b/spec/fixtures/rentals_united/cancel/success.xml @@ -0,0 +1,3 @@ + + Success + diff --git a/spec/fixtures/rentals_united/location_currencies/currencies.xml b/spec/fixtures/rentals_united/location_currencies/currencies.xml new file mode 100644 index 000000000..65568a6d2 --- /dev/null +++ b/spec/fixtures/rentals_united/location_currencies/currencies.xml @@ -0,0 +1,20 @@ + + Success + + + + 7892 + + + + + 4530 + 4977 + + + + + + + + diff --git a/spec/fixtures/rentals_united/location_currencies/error_status.xml b/spec/fixtures/rentals_united/location_currencies/error_status.xml new file mode 100644 index 000000000..7da93453d --- /dev/null +++ b/spec/fixtures/rentals_united/location_currencies/error_status.xml @@ -0,0 +1,3 @@ + + Test Error + diff --git a/spec/fixtures/rentals_united/locations/error_status.xml b/spec/fixtures/rentals_united/locations/error_status.xml new file mode 100644 index 000000000..fd7655cf8 --- /dev/null +++ b/spec/fixtures/rentals_united/locations/error_status.xml @@ -0,0 +1,3 @@ + + Test Error + diff --git a/spec/fixtures/rentals_united/locations/locations.xml b/spec/fixtures/rentals_united/locations/locations.xml new file mode 100644 index 000000000..44be0384a --- /dev/null +++ b/spec/fixtures/rentals_united/locations/locations.xml @@ -0,0 +1,18 @@ + + Success + + Worldwide + Europe + France + Germany + Greece + Hungary + Languedoc-Roussillon + Ile-de-France + Haute-Normandie + Pantin + Paris + Plaisir + Neighborhood + + diff --git a/spec/fixtures/rentals_united/locations/no_parent_for_city.xml b/spec/fixtures/rentals_united/locations/no_parent_for_city.xml new file mode 100644 index 000000000..44aab1776 --- /dev/null +++ b/spec/fixtures/rentals_united/locations/no_parent_for_city.xml @@ -0,0 +1,18 @@ + + Success + + Worldwide + Europe + France + Germany + Greece + Hungary + Languedoc-Roussillon + Ile-de-France + Haute-Normandie + Pantin + Paris + Plaisir + Neighborhood + + diff --git a/spec/fixtures/rentals_united/locations/no_parent_for_country.xml b/spec/fixtures/rentals_united/locations/no_parent_for_country.xml new file mode 100644 index 000000000..bd21e8143 --- /dev/null +++ b/spec/fixtures/rentals_united/locations/no_parent_for_country.xml @@ -0,0 +1,18 @@ + + Success + + Worldwide + Europe + France + Germany + Greece + Hungary + Languedoc-Roussillon + Ile-de-France + Haute-Normandie + Pantin + Paris + Plaisir + Neighborhood + + diff --git a/spec/fixtures/rentals_united/locations/no_parent_for_neighborhood.xml b/spec/fixtures/rentals_united/locations/no_parent_for_neighborhood.xml new file mode 100644 index 000000000..47b1d8f92 --- /dev/null +++ b/spec/fixtures/rentals_united/locations/no_parent_for_neighborhood.xml @@ -0,0 +1,18 @@ + + Success + + Worldwide + Europe + France + Germany + Greece + Hungary + Languedoc-Roussillon + Ile-de-France + Haute-Normandie + Pantin + Paris + Plaisir + Neighborhood + + diff --git a/spec/fixtures/rentals_united/locations/no_parent_for_region.xml b/spec/fixtures/rentals_united/locations/no_parent_for_region.xml new file mode 100644 index 000000000..4c7ef3c4d --- /dev/null +++ b/spec/fixtures/rentals_united/locations/no_parent_for_region.xml @@ -0,0 +1,18 @@ + + Success + + Worldwide + Europe + France + Germany + Greece + Hungary + Languedoc-Roussillon + Ile-de-France + Haute-Normandie + Pantin + Paris + Plaisir + Neighborhood + + diff --git a/spec/fixtures/rentals_united/owner/error_status.xml b/spec/fixtures/rentals_united/owner/error_status.xml new file mode 100644 index 000000000..ef296d223 --- /dev/null +++ b/spec/fixtures/rentals_united/owner/error_status.xml @@ -0,0 +1,3 @@ + + Test Error + diff --git a/spec/fixtures/rentals_united/owner/not_found.xml b/spec/fixtures/rentals_united/owner/not_found.xml new file mode 100644 index 000000000..9d7ec7a7b --- /dev/null +++ b/spec/fixtures/rentals_united/owner/not_found.xml @@ -0,0 +1 @@ + diff --git a/spec/fixtures/rentals_united/owner/owner.xml b/spec/fixtures/rentals_united/owner/owner.xml new file mode 100644 index 000000000..343283e79 --- /dev/null +++ b/spec/fixtures/rentals_united/owner/owner.xml @@ -0,0 +1,10 @@ + + Success + + Foo + Bar + RU Test + foobar@gmail.com + 519461272 + + diff --git a/spec/fixtures/rentals_united/properties/archived.xml b/spec/fixtures/rentals_united/properties/archived.xml new file mode 100644 index 000000000..37473f470 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/archived.xml @@ -0,0 +1,10 @@ + + Success + + -1 + 519688 + true + true + 35 + + diff --git a/spec/fixtures/rentals_united/properties/basement_floor.xml b/spec/fixtures/rentals_united/properties/basement_floor.xml new file mode 100644 index 000000000..ec2fccce8 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/basement_floor.xml @@ -0,0 +1,81 @@ + + Success + + -1 + 519688 + Test property + 427698 + 24958 + 2016-08-31 11:39:48 + 2016-08-31 + true + true + 427698 + true + false + 2.6500 + 39 + 2 + 2 + 4 + 35 + -1000 + Test street address + 644119 + + 55.0003426 + 73.2965942999999 + + + Ruslan Sharipov + sharipov.reg@gmail.com + +79618492980 + 1 + + + + + 13:00 + 17:00 + 11:00 + at_the_apartment + + + + 99.6000 + 5.50 + + + + 7 + 100 + 180 + 187 + 227 + 281 + 368 + 596 + 689 + 802 + 803 + + + + + + + + https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082398988145159.jpg + https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082399089701851.jpg + + + + + + + + + + + + diff --git a/spec/fixtures/rentals_united/properties/error_status.xml b/spec/fixtures/rentals_united/properties/error_status.xml new file mode 100644 index 000000000..0c4386de0 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/error_status.xml @@ -0,0 +1,3 @@ + + Test Error + diff --git a/spec/fixtures/rentals_united/properties/not_active.xml b/spec/fixtures/rentals_united/properties/not_active.xml new file mode 100644 index 000000000..28b01574e --- /dev/null +++ b/spec/fixtures/rentals_united/properties/not_active.xml @@ -0,0 +1,10 @@ + + Success + + -1 + 519688 + false + false + 35 + + diff --git a/spec/fixtures/rentals_united/properties/not_found.xml b/spec/fixtures/rentals_united/properties/not_found.xml new file mode 100644 index 000000000..b224fc4e3 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/not_found.xml @@ -0,0 +1,3 @@ + + Property does not exist. + diff --git a/spec/fixtures/rentals_united/properties/property.xml b/spec/fixtures/rentals_united/properties/property.xml new file mode 100644 index 000000000..6a291cb2b --- /dev/null +++ b/spec/fixtures/rentals_united/properties/property.xml @@ -0,0 +1,81 @@ + + Success + + -1 + 519688 + Test property + 427698 + 24958 + 2016-08-31 11:39:48 + 2016-08-31 + true + true + 427698 + true + false + 2.6500 + 39 + 2 + 2 + 4 + 35 + 3 + Test street address + 644119 + + 55.0003426 + 73.2965942999999 + + + Ruslan Sharipov + sharipov.reg@gmail.com + +79618492980 + 1 + + + + + 13:00 + 17:00 + 11:00 + at_the_apartment + + + + 99.6000 + 5.50 + + + + 7 + 100 + 180 + 187 + 227 + 281 + 368 + 596 + 689 + 802 + 803 + + + + + + + + https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082398988145159.jpg + https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082399089701851.jpg + + + + + + + + + + + + diff --git a/spec/fixtures/rentals_united/properties/property_with_multiple_descriptions.xml b/spec/fixtures/rentals_united/properties/property_with_multiple_descriptions.xml new file mode 100644 index 000000000..0b16f5d0f --- /dev/null +++ b/spec/fixtures/rentals_united/properties/property_with_multiple_descriptions.xml @@ -0,0 +1,19 @@ + + Success + + 519688 + true + false + 35 + 99.6000 + 5.50 + + + + + + + + + + diff --git a/spec/fixtures/rentals_united/properties/property_with_one_image.xml b/spec/fixtures/rentals_united/properties/property_with_one_image.xml new file mode 100644 index 000000000..682465c85 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/property_with_one_image.xml @@ -0,0 +1,15 @@ + + Success + + -1 + 519688 + true + false + 35 + 99.6000 + 5.50 + + https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082399089701851.jpg + + + diff --git a/spec/fixtures/rentals_united/properties/property_without_amenities.xml b/spec/fixtures/rentals_united/properties/property_without_amenities.xml new file mode 100644 index 000000000..f41fac1b7 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/property_without_amenities.xml @@ -0,0 +1,12 @@ + + Success + + -1 + 519688 + true + false + 35 + 99.6000 + 5.50 + + diff --git a/spec/fixtures/rentals_united/properties/property_without_descriptions.xml b/spec/fixtures/rentals_united/properties/property_without_descriptions.xml new file mode 100644 index 000000000..f41fac1b7 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/property_without_descriptions.xml @@ -0,0 +1,12 @@ + + Success + + -1 + 519688 + true + false + 35 + 99.6000 + 5.50 + + diff --git a/spec/fixtures/rentals_united/properties/property_without_images.xml b/spec/fixtures/rentals_united/properties/property_without_images.xml new file mode 100644 index 000000000..f41fac1b7 --- /dev/null +++ b/spec/fixtures/rentals_united/properties/property_without_images.xml @@ -0,0 +1,12 @@ + + Success + + -1 + 519688 + true + false + 35 + 99.6000 + 5.50 + + diff --git a/spec/fixtures/rentals_united/properties_collection/empty_list.xml b/spec/fixtures/rentals_united/properties_collection/empty_list.xml new file mode 100644 index 000000000..4bbdbfeb8 --- /dev/null +++ b/spec/fixtures/rentals_united/properties_collection/empty_list.xml @@ -0,0 +1,4 @@ + + Success + + diff --git a/spec/fixtures/rentals_united/properties_collection/error_status.xml b/spec/fixtures/rentals_united/properties_collection/error_status.xml new file mode 100644 index 000000000..691dafbb3 --- /dev/null +++ b/spec/fixtures/rentals_united/properties_collection/error_status.xml @@ -0,0 +1,3 @@ + + Test Error + diff --git a/spec/fixtures/rentals_united/properties_collection/multiple_properties.xml b/spec/fixtures/rentals_united/properties_collection/multiple_properties.xml new file mode 100644 index 000000000..8f47efc0a --- /dev/null +++ b/spec/fixtures/rentals_united/properties_collection/multiple_properties.xml @@ -0,0 +1,27 @@ + + Success + + + 519688 + Test property + 427698 + 24958 + 2016-08-31 11:39:48 + 2016-08-31 + true + true + 0 + + + 519689 + Test property + 427698 + 24958 + 2016-08-31 11:39:48 + 2016-08-31 + true + true + 0 + + + diff --git a/spec/fixtures/rentals_united/properties_collection/one_property.xml b/spec/fixtures/rentals_united/properties_collection/one_property.xml new file mode 100644 index 000000000..77bff5537 --- /dev/null +++ b/spec/fixtures/rentals_united/properties_collection/one_property.xml @@ -0,0 +1,16 @@ + + Success + + + 519688 + Test property + 427698 + 24958 + 2016-08-31 11:39:48 + 2016-08-31 + true + true + 0 + + + diff --git a/spec/fixtures/rentals_united/quotations/invalid_date_from.xml b/spec/fixtures/rentals_united/quotations/invalid_date_from.xml new file mode 100644 index 000000000..e33af1996 --- /dev/null +++ b/spec/fixtures/rentals_united/quotations/invalid_date_from.xml @@ -0,0 +1,3 @@ + + DateFrom has to be earlier than DateTo. + diff --git a/spec/fixtures/rentals_united/quotations/invalid_max_guests.xml b/spec/fixtures/rentals_united/quotations/invalid_max_guests.xml new file mode 100644 index 000000000..befe6008f --- /dev/null +++ b/spec/fixtures/rentals_united/quotations/invalid_max_guests.xml @@ -0,0 +1,3 @@ + + NOP: positive value required. + diff --git a/spec/fixtures/rentals_united/quotations/not_available.xml b/spec/fixtures/rentals_united/quotations/not_available.xml new file mode 100644 index 000000000..ad17ad633 --- /dev/null +++ b/spec/fixtures/rentals_united/quotations/not_available.xml @@ -0,0 +1,3 @@ + + Property is not available for a given dates + diff --git a/spec/fixtures/rentals_united/quotations/success.xml b/spec/fixtures/rentals_united/quotations/success.xml new file mode 100644 index 000000000..4547c0ad3 --- /dev/null +++ b/spec/fixtures/rentals_united/quotations/success.xml @@ -0,0 +1,6 @@ + + Success + + 284.50 + + diff --git a/spec/fixtures/rentals_united/quotations/too_many_guests.xml b/spec/fixtures/rentals_united/quotations/too_many_guests.xml new file mode 100644 index 000000000..a61fdcaa0 --- /dev/null +++ b/spec/fixtures/rentals_united/quotations/too_many_guests.xml @@ -0,0 +1,3 @@ + + Number of guests exceedes the maximum allowed. + diff --git a/spec/fixtures/rentals_united/reservations/not_available.xml b/spec/fixtures/rentals_united/reservations/not_available.xml new file mode 100644 index 000000000..74610004e --- /dev/null +++ b/spec/fixtures/rentals_united/reservations/not_available.xml @@ -0,0 +1,4 @@ + + Property is not available for a given dates + 0 + diff --git a/spec/fixtures/rentals_united/reservations/success.xml b/spec/fixtures/rentals_united/reservations/success.xml new file mode 100644 index 000000000..208c98922 --- /dev/null +++ b/spec/fixtures/rentals_united/reservations/success.xml @@ -0,0 +1,4 @@ + + Success + 90377000 + diff --git a/spec/fixtures/rentals_united/seasons/no_seasons.xml b/spec/fixtures/rentals_united/seasons/no_seasons.xml new file mode 100644 index 000000000..698e09bcb --- /dev/null +++ b/spec/fixtures/rentals_united/seasons/no_seasons.xml @@ -0,0 +1,4 @@ + + Success + + diff --git a/spec/fixtures/rentals_united/seasons/not_found.xml b/spec/fixtures/rentals_united/seasons/not_found.xml new file mode 100644 index 000000000..635ee2c76 --- /dev/null +++ b/spec/fixtures/rentals_united/seasons/not_found.xml @@ -0,0 +1,3 @@ + + Property does not exist. + diff --git a/spec/fixtures/rentals_united/seasons/success.xml b/spec/fixtures/rentals_united/seasons/success.xml new file mode 100644 index 000000000..a2f717115 --- /dev/null +++ b/spec/fixtures/rentals_united/seasons/success.xml @@ -0,0 +1,13 @@ + + Success + + + 200.0000 + 10.0000 + + + 170.0000 + 5.0000 + + + diff --git a/spec/fixtures/waytostay/bookings/quote.json b/spec/fixtures/waytostay/bookings/quote.json index 25706cee8..a56e66481 100644 --- a/spec/fixtures/waytostay/bookings/quote.json +++ b/spec/fixtures/waytostay/bookings/quote.json @@ -1,6 +1,6 @@ { "booking_details": { - "property_reference": "016111", + "property_reference": "success", "arrival_date": "2016-07-14", "departure_date": "2016-07-17", "number_of_nights": 3, diff --git a/spec/lib/concierge/suppliers/atleisure/price_spec.rb b/spec/lib/concierge/suppliers/atleisure/price_spec.rb index 93c898be0..61a854b8a 100644 --- a/spec/lib/concierge/suppliers/atleisure/price_spec.rb +++ b/spec/lib/concierge/suppliers/atleisure/price_spec.rb @@ -6,13 +6,13 @@ include Support::Factories let(:credentials) { double(username: "roomorama", password: "atleisure-roomorama") } + let(:supplier) { create_supplier(name: AtLeisure::Client::SUPPLIER_NAME) } + let!(:host) { create_host(supplier_id: supplier.id, fee_percentage: 7.0) } let(:params) { { property_id: "AT-123", check_in: "2016-03-22", check_out: "2016-03-25", guests: 2 } } before do - supplier = create_supplier(name: AtLeisure::Client::SUPPLIER_NAME) - create_host(supplier_id: supplier.id, fee_percentage: 7.0) allow_any_instance_of(Concierge::JSONRPC).to receive(:request_id) { 888888888888 } end @@ -105,6 +105,7 @@ end it "returns an available quotation properly priced according to the response" do + create_property(identifier: "AT-123", host_id: host.id) stub_with_fixture("atleisure/available.json") result = subject.quote(params) @@ -122,14 +123,6 @@ expect(quotation.host_fee_percentage).to eq(7) end - it 'fails if host is not found' do - allow(subject).to receive(:fetch_host) { nil } - result = subject.quote(params) - - expect(result).not_to be_success - expect(result.error.code).to eq :host_not_found - end - def stub_with_fixture(name) atleisure_response = JSON.parse(read_fixture(name)) response = { diff --git a/spec/lib/concierge/suppliers/ciirus/price_spec.rb b/spec/lib/concierge/suppliers/ciirus/price_spec.rb deleted file mode 100644 index dc9faa4b3..000000000 --- a/spec/lib/concierge/suppliers/ciirus/price_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "spec_helper" - -RSpec.describe Ciirus::Price do - include Support::Fixtures - include Support::Factories - - let!(:host) { create_host(fee_percentage: 7) } - let!(:property) { create_property(identifier: '123', host_id: host.id) } - let(:credentials) do - double(username: 'Foo', - password: '123', - url: 'http://proxy.roomorama.com/ciirus') - end - let(:params) do - API::Controllers::Params::Quote.new(property_id: '123', - check_in: '2017-08-01', - check_out: '2017-08-05', - guests: 2) - end - - subject { described_class.new(credentials) } - - describe '#quote' do - it 'fails if property is not found' do - params.property_id = 'unknown id' - result = subject.quote(params) - - expect(result).not_to be_success - expect(result.error.code).to eq :property_not_found - end - - it 'fails if host is not found' do - allow(subject).to receive(:fetch_host) { nil } - result = subject.quote(params) - - expect(result).not_to be_success - expect(result.error.code).to eq :host_not_found - end - - it 'fills host_fee_percentage' do - allow_any_instance_of(Ciirus::Commands::QuoteFetcher).to receive(:call) { Result.new(Quotation.new) } - - result = subject.quote(params) - expect(result).to be_success - expect(result.value.host_fee_percentage).to eq(7) - end - end -end \ No newline at end of file diff --git a/spec/lib/concierge/suppliers/kigo/booking_spec.rb b/spec/lib/concierge/suppliers/kigo/booking_spec.rb index d1791cd35..edf7efcb5 100644 --- a/spec/lib/concierge/suppliers/kigo/booking_spec.rb +++ b/spec/lib/concierge/suppliers/kigo/booking_spec.rb @@ -2,6 +2,7 @@ RSpec.describe Kigo::Booking do include Support::Fixtures + include Support::Factories include Support::HTTPStubbing let(:credentials) { double(subscription_key: '32933') } @@ -41,7 +42,6 @@ end it 'returns wrapped reservation with code if success' do - allow_any_instance_of(Kigo::ResponseParser).to receive(:host) { Host.new(fee_percentage: 0) } stub_call(:post, endpoint) { [200, {}, read_fixture('kigo/success_booking.json')] } result = subject.book(params) diff --git a/spec/lib/concierge/suppliers/kigo/response_parser_spec.rb b/spec/lib/concierge/suppliers/kigo/response_parser_spec.rb index eb2035791..c6c835bf1 100644 --- a/spec/lib/concierge/suppliers/kigo/response_parser_spec.rb +++ b/spec/lib/concierge/suppliers/kigo/response_parser_spec.rb @@ -77,27 +77,6 @@ expect(quotation.available).to eq false end - it "fails if property not found" do - request_params[:property_id] = 'unknown id' - - subject = described_class.new(request_params) - - response = read_fixture("kigo/success.json") - result = subject.compute_pricing(response) - - expect(result).not_to be_success - expect(result.error.code).to eq :property_not_found - end - - it "fails if host not found" do - allow(subject).to receive(:host) { nil } - response = read_fixture("kigo/success.json") - result = subject.compute_pricing(response) - - expect(result).not_to be_success - expect(result.error.code).to eq :host_not_found - end - it "returns a quotation with the returned information on success" do response = read_fixture("kigo/success.json") result = subject.compute_pricing(response) @@ -119,7 +98,8 @@ it "returns net ammount if host has a fee" do host.fee_percentage = 8.0 - allow(subject).to receive(:host) { host } + HostRepository.update host + #allow(subject).to receive(:host) { host } response = read_fixture("kigo/success.json") result = subject.compute_pricing(response) diff --git a/spec/lib/concierge/suppliers/poplidays/mappers/quote_spec.rb b/spec/lib/concierge/suppliers/poplidays/mappers/quote_spec.rb index cd34bb9e8..bc6d912ed 100644 --- a/spec/lib/concierge/suppliers/poplidays/mappers/quote_spec.rb +++ b/spec/lib/concierge/suppliers/poplidays/mappers/quote_spec.rb @@ -2,9 +2,11 @@ RSpec.describe Poplidays::Mappers::Quote do include Support::Fixtures + include Support::Factories + let(:host) { create_host(fee_percentage: 5.0) } + let!(:property) { create_property(identifier: '33680', host_id: host.id) } let(:mandatory_services) { 25.0 } - let(:host_fee_percentage) { 5.0 } let(:params) do API::Controllers::Params::Quote.new(property_id: '33680', check_in: '2017-08-01', @@ -13,7 +15,7 @@ end subject { described_class.new } - let(:result) { subject.build(params, mandatory_services, quote, host_fee_percentage) } + let(:result) { subject.build(params, mandatory_services, quote) } context 'for success response' do let(:quote) do @@ -68,4 +70,4 @@ end -end \ No newline at end of file +end diff --git a/spec/lib/concierge/suppliers/poplidays/price_spec.rb b/spec/lib/concierge/suppliers/poplidays/price_spec.rb index 21c4bcf17..ad29941e3 100644 --- a/spec/lib/concierge/suppliers/poplidays/price_spec.rb +++ b/spec/lib/concierge/suppliers/poplidays/price_spec.rb @@ -7,6 +7,7 @@ let!(:supplier) { create_supplier(name: Poplidays::Client::SUPPLIER_NAME) } let!(:host) { create_host(supplier_id: supplier.id, fee_percentage: 5) } + let!(:property) { create_property(identifier: '3498', host_id: host.id) } let(:params) { { property_id: '3498', check_in: '2016-12-17', check_out: '2016-12-26', guests: 2 } } @@ -50,14 +51,6 @@ expect(result.error.code).to eq :connection_timeout end - it 'fails if host is not found' do - allow(subject).to receive(:fetch_host) { nil } - result = subject.quote(params) - - expect(result).not_to be_success - expect(result.error.code).to eq :host_not_found - end - it 'returns the underlying network error if any happened in the call for the quote endpoint' do stub_with_fixture(property_details_endpoint, 'poplidays/property_details.json') stub_call(:post, quote_endpoint) { raise Faraday::TimeoutError } diff --git a/spec/lib/concierge/suppliers/rentals_united/client_spec.rb b/spec/lib/concierge/suppliers/rentals_united/client_spec.rb new file mode 100644 index 000000000..2016f339e --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/client_spec.rb @@ -0,0 +1,77 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Client do + include Support::Factories + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:client) { described_class.new(credentials) } + + describe "#quote" do + before do + supplier = create_supplier(name: supplier_name) + host = create_host(identifier: "ru-host", supplier_id: supplier.id) + create_property(identifier: '1234', host_id: host.id, data: { :currency => "USD" }) + end + + let(:quotation_params) do + { + property_id: '1234', + check_in: '2016-02-02', + check_out: '2016-02-03', + guests: 2 + } + end + + it "returns error if property does not exist" do + quotation_params[:property_id] = "unknown" + + fetcher_class = RentalsUnited::Commands::PriceFetcher + expect_any_instance_of(fetcher_class).not_to(receive(:call)) + + result = client.quote(quotation_params) + expect(result.success?).to be false + expect(result.error.code).to eq(:property_not_found) + end + + it "calls price fetcher class if property exists" do + price = RentalsUnited::Entities::Price.new( + total: 123.45, + available: true + ) + + fetcher_class = RentalsUnited::Commands::PriceFetcher + expect_any_instance_of(fetcher_class) + .to(receive(:call)) + .and_return(Result.new(price)) + + result = client.quote(quotation_params) + expect(result.success?).to be true + + quotation = result.value + expect(quotation).to be_kind_of(Quotation) + end + end + + describe "#book" do + let(:params) { {} } + + it "calls quotation fetcher class" do + fetcher_class = RentalsUnited::Commands::Booking + + expect_any_instance_of(fetcher_class).to(receive(:call)) + client.book(params) + end + end + + describe "#cancel" do + let(:params) { { reference_number: '555444' } } + + it "calls cancel command class" do + fetcher_class = RentalsUnited::Commands::Cancel + + expect_any_instance_of(fetcher_class).to(receive(:call)) + client.cancel(params) + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/availabilities_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/availabilities_fetcher_spec.rb new file mode 100644 index 000000000..6fd069a22 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/availabilities_fetcher_spec.rb @@ -0,0 +1,79 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::AvailabilitiesFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:property_id) { "1234" } + let(:subject) { described_class.new(credentials, property_id) } + let(:url) { credentials.url } + + it "returns an error if property does not exist" do + stub_data = read_fixture("rentals_united/availabilities/not_found.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_availabilities + expect(result).not_to be_success + expect(result.error.code).to eq("56") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `56`, and description `Property does not exist.`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + context "when response contains availability data" do + let(:file_name) { "rentals_united/availabilities/success.xml" } + + before do + stub_data = read_fixture(file_name) + stub_call(:post, url) { [200, {}, stub_data] } + end + + it "returns availabilities" do + result = subject.fetch_availabilities + expect(result).to be_success + expect(result.value.size).to eq(24) + expect(result.value).to all( + be_kind_of(RentalsUnited::Entities::Availability) + ) + end + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_availabilities + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.fetch_availabilities + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/booking_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/booking_spec.rb new file mode 100644 index 000000000..735d2b0eb --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/booking_spec.rb @@ -0,0 +1,90 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::Booking do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:reservation_params) do + API::Controllers::Params::Booking.new( + property_id: '1', + check_in: '2016-02-02', + check_out: '2016-02-03', + guests: 1, + currency_code: 'EUR', + subtotal: '123.45', + customer: { + first_name: 'Test', + last_name: 'User', + email: 'testuser@example.com', + phone: '111-222-3333', + display: 'Test User' + } + ) + end + let(:subject) { described_class.new(credentials, reservation_params) } + + it "successfully creates a reservation" do + stub_data = read_fixture("rentals_united/reservations/success.xml") + stub_call(:post, credentials.url) { [200, {}, stub_data] } + + result = subject.call + expect(result.success?).to be true + expect(result.value).to be_kind_of(Reservation) + expect(result.value.reference_number).to eq("90377000") + expect(result.value.property_id).to eq("1") + expect(result.value.check_in).to eq("2016-02-02") + expect(result.value.check_out).to eq("2016-02-03") + expect(result.value.guests).to eq(1) + end + + it "fails when property is not available for a given dates" do + stub_data = read_fixture("rentals_united/reservations/not_available.xml") + stub_call(:post, credentials.url) { [200, {}, stub_data] } + + result = subject.call + expect(result).not_to be_success + expect(result.error.code).to eq("1") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `1`, and description `Property is not available for a given dates`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, credentials.url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, credentials.url) { raise Faraday::TimeoutError } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/cancel_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/cancel_spec.rb new file mode 100644 index 000000000..6cc8cbe1e --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/cancel_spec.rb @@ -0,0 +1,75 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::Cancel do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:reference_number) { "888999777" } + let(:subject) { described_class.new(credentials, reference_number) } + let(:url) { credentials.url } + + it "performs successful cancel request" do + stub_data = read_fixture("rentals_united/cancel/success.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).to be_kind_of(Result) + expect(result).to be_success + + expect(result.value).to eq(reference_number) + end + + it "returns error if reservation does not exist" do + stub_data = read_fixture("rentals_united/cancel/does_not_exist.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).to be_kind_of(Result) + expect(result).not_to be_success + expect(result.error.code).to eq("28") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `28`, and description `Reservation does not exist.`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/location_currencies_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/location_currencies_fetcher_spec.rb new file mode 100644 index 000000000..d464198a0 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/location_currencies_fetcher_spec.rb @@ -0,0 +1,85 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::LocationCurrenciesFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:subject) { described_class.new(credentials) } + let(:url) { credentials.url } + + it "fetches currencies for locations" do + stub_data = read_fixture("rentals_united/location_currencies/currencies.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_location_currencies + expect(result).to be_success + expect(result.value.size).to eq(3) + expect(result.value["7892"]).to eq("AUD") + expect(result.value["4530"]).to eq("CAD") + expect(result.value["4977"]).to eq("CAD") + end + + it "returns nil locations which has no currency" do + stub_data = read_fixture("rentals_united/location_currencies/currencies.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_location_currencies + expect(result).to be_success + expect(result.value.size).to eq(3) + expect(result.value["1111"]).to be_nil + end + + context "when response from the api has error status" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/location_currencies/error_status.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_location_currencies + + expect(result).not_to be_success + expect(result.error.code).to eq("9999") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `9999`, and description ``" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_location_currencies + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.fetch_location_currencies + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/locations_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/locations_fetcher_spec.rb new file mode 100644 index 000000000..f9c07d067 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/locations_fetcher_spec.rb @@ -0,0 +1,214 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::LocationsFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:url) { credentials.url } + + context "when location does not exist" do + let(:location_ids) { ["9998"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "returns error" do + stub_data = read_fixture("rentals_united/locations/locations.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).not_to be_success + expect(result.error.code).to eq(:unknown_location) + end + end + + context "when fetching location for neighborhood" do + let(:location_ids) { ["9999"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "fetches and returns one location" do + stub_data = read_fixture("rentals_united/locations/locations.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).to be_success + expect(result.value.size).to eq(1) + + location = result.value.first + expect(location).to be_kind_of(RentalsUnited::Entities::Location) + expect(location.id).to eq("9999") + expect(location.city).to eq("Paris") + expect(location.region).to eq("Ile-de-France") + expect(location.country).to eq("France") + expect(location.neighborhood).to eq("Neighborhood") + end + + context "when parent location does not exist" do + it "returns error" do + stub_data = read_fixture("rentals_united/locations/no_parent_for_neighborhood.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).not_to be_success + expect(result.error.code).to eq(:unknown_location) + end + end + end + + context "when fetching location for city" do + let(:location_ids) { ["1505"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "fetches and returns one location" do + stub_data = read_fixture("rentals_united/locations/locations.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).to be_success + expect(result.value.size).to eq(1) + + location = result.value.first + expect(location).to be_kind_of(RentalsUnited::Entities::Location) + expect(location.id).to eq("1505") + expect(location.city).to eq("Paris") + expect(location.region).to eq("Ile-de-France") + expect(location.country).to eq("France") + end + + context "when parent location does not exist" do + it "returns error" do + stub_data = read_fixture("rentals_united/locations/no_parent_for_city.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).not_to be_success + expect(result.error.code).to eq(:unknown_location) + end + end + end + + context "when fetching location for region" do + let(:location_ids) { ["10351"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "fetches and returns one location" do + stub_data = read_fixture("rentals_united/locations/locations.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).to be_success + expect(result.value.size).to eq(1) + + location = result.value.first + expect(location).to be_kind_of(RentalsUnited::Entities::Location) + expect(location.id).to eq("10351") + expect(location.city).to eq(nil) + expect(location.region).to eq("Ile-de-France") + expect(location.country).to eq("France") + end + + context "when parent location does not exist" do + it "returns error" do + stub_data = read_fixture("rentals_united/locations/no_parent_for_region.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).not_to be_success + expect(result.error.code).to eq(:unknown_location) + end + end + end + + context "when fetching location for country" do + let(:location_ids) { ["20"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "fetches and returns one location" do + stub_data = read_fixture("rentals_united/locations/locations.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).to be_success + expect(result.value.size).to eq(1) + + location = result.value.first + expect(location).to be_kind_of(RentalsUnited::Entities::Location) + expect(location.id).to eq("20") + expect(location.city).to eq(nil) + expect(location.region).to eq(nil) + expect(location.country).to eq("France") + end + + context "when parent location does not exist" do + it "returns location anyway" do + stub_data = read_fixture("rentals_united/locations/no_parent_for_country.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + expect(result).not_to be_success + expect(result.error.code).to eq(:unknown_location) + end + end + end + + context "when response from the api has error status" do + let(:location_ids) { ["1505"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/locations/error_status.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + + expect(result).not_to be_success + expect(result.error.code).to eq("9999") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `9999`, and description ``" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when response from the api is not well-formed xml" do + let(:location_ids) { ["1505"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_locations + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + let(:location_ids) { ["1505"] } + let(:subject) { described_class.new(credentials, location_ids) } + + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.fetch_locations + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/owner_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/owner_fetcher_spec.rb new file mode 100644 index 000000000..e9ddc735d --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/owner_fetcher_spec.rb @@ -0,0 +1,89 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::OwnerFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:owner_id) { '123' } + let(:subject) { described_class.new(credentials, owner_id) } + let(:url) { credentials.url } + + it "fetches owner" do + stub_data = read_fixture("rentals_united/owner/owner.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_owner + expect(result).to be_success + + owner = result.value + expect(owner).to be_kind_of(RentalsUnited::Entities::Owner) + expect(owner.id).to eq("419680") + expect(owner.first_name).to eq("Foo") + expect(owner.last_name).to eq("Bar") + expect(owner.email).to eq("foobar@gmail.com") + expect(owner.phone).to eq("519461272") + end + + it "returns error when there is no requested owner" do + stub_data = read_fixture("rentals_united/owner/not_found.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_owner + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + end + + context "when response from the api has error status" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/owner/error_status.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_owner + + expect(result).not_to be_success + expect(result.error.code).to eq("9999") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `9999`, and description ``" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_owner + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.fetch_owner + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/price_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/price_fetcher_spec.rb new file mode 100644 index 000000000..28f6c873f --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/price_fetcher_spec.rb @@ -0,0 +1,134 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::PriceFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:property_id) { "1234" } + let(:stay_params) do + API::Controllers::Params::Quote.new( + property_id: '1234', + check_in: "2016-09-19", + check_out: "2016-09-20", + guests: 3 + ) + end + let(:subject) { described_class.new(credentials, stay_params) } + let(:url) { credentials.url } + + it "performs successful request returning Entities::Price object" do + stub_data = read_fixture("rentals_united/quotations/success.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).to be_kind_of(Result) + expect(result).to be_success + + price = result.value + expect(price).to be_kind_of(RentalsUnited::Entities::Price) + expect(price.total).to eq(284.5) + expect(price.available?).to eq(true) + end + + it "returns unavailable Quotation when property is not available" do + stub_data = read_fixture("rentals_united/quotations/not_available.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).to be_kind_of(Result) + expect(result).to be_success + + price = result.value + expect(price).to be_kind_of(RentalsUnited::Entities::Price) + expect(price.total).to eq(0) + expect(price.available?).to eq(false) + end + + it "returns an error when check_in is invalid" do + stub_data = read_fixture("rentals_united/quotations/invalid_date_from.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq("74") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `74`, and description `DateFrom has to be earlier than DateTo.`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + it "returns an error when num_guests are greater than allowed" do + stub_data = read_fixture("rentals_united/quotations/too_many_guests.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq("76") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `76`, and description `Number of guests exceedes the maximum allowed.`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + it "returns an error when num_guests are invalid" do + stub_data = read_fixture("rentals_united/quotations/invalid_max_guests.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq("77") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `77`, and description `NOP: positive value required.`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.call + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/properties_collection_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/properties_collection_fetcher_spec.rb new file mode 100644 index 000000000..777bbb225 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/properties_collection_fetcher_spec.rb @@ -0,0 +1,112 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::PropertiesCollectionFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:owner_id) { "1234" } + let(:subject) { described_class.new(credentials, owner_id) } + let(:url) { credentials.url } + + it "returns an empty collection when there is no properties in location" do + stub_data = read_fixture("rentals_united/properties_collection/empty_list.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_properties_collection_for_owner + expect(result).to be_success + + collection = result.value + expect(collection).to be_kind_of( + RentalsUnited::Entities::PropertiesCollection + ) + expect(collection.size).to eq(0) + expect(collection.property_ids).to eq([]) + expect(collection.location_ids).to eq([]) + end + + it "returns collection when there is only one property" do + stub_data = read_fixture("rentals_united/properties_collection/one_property.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_properties_collection_for_owner + expect(result).to be_success + + collection = result.value + expect(collection).to be_kind_of( + RentalsUnited::Entities::PropertiesCollection + ) + expect(collection.size).to eq(1) + expect(collection.property_ids).to eq(["519688"]) + expect(collection.location_ids).to eq(["24958"]) + end + + it "returns collection with multiple objects" do + stub_data = read_fixture("rentals_united/properties_collection/multiple_properties.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_properties_collection_for_owner + expect(result).to be_success + + collection = result.value + expect(collection).to be_kind_of( + RentalsUnited::Entities::PropertiesCollection + ) + expect(collection.size).to eq(2) + expect(collection.property_ids).to eq(["519688", "519689"]) + expect(collection.location_ids).to eq(["24958"]) + end + + context "when response from the api has error status" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/properties_collection/error_status.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_properties_collection_for_owner + + expect(result).not_to be_success + expect(result.error.code).to eq("9999") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `9999`, and description ``" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_properties_collection_for_owner + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.fetch_properties_collection_for_owner + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/property_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/property_fetcher_spec.rb new file mode 100644 index 000000000..6ec247784 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/property_fetcher_spec.rb @@ -0,0 +1,85 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::PropertyFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:property_id) { "1234" } + let(:subject) { described_class.new(credentials, property_id) } + let(:url) { credentials.url } + + it "returns an error if property does not exist" do + stub_data = read_fixture("rentals_united/properties/not_found.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_property + expect(result).not_to be_success + expect(result.error.code).to eq("56") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `56`, and description `Property does not exist.`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + context "when response contains property data" do + let(:file_name) { "rentals_united/properties/property.xml" } + + before do + stub_data = read_fixture(file_name) + stub_call(:post, url) { [200, {}, stub_data] } + end + + let(:property) do + result = subject.fetch_property + expect(result).to be_success + + result.value + end + + it "returns property object" do + expect(property).to be_kind_of(RentalsUnited::Entities::Property) + end + + it "sets id to the property" do + expect(property.id).to eq("519688") + end + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_property + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.fetch_property + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/commands/seasons_fetcher_spec.rb b/spec/lib/concierge/suppliers/rentals_united/commands/seasons_fetcher_spec.rb new file mode 100644 index 000000000..470163a5f --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/commands/seasons_fetcher_spec.rb @@ -0,0 +1,86 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Commands::SeasonsFetcher do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:property_id) { "1234" } + let(:subject) { described_class.new(credentials, property_id) } + let(:url) { credentials.url } + + it "returns an error if property does not exist" do + stub_data = read_fixture("rentals_united/seasons/not_found.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_seasons + expect(result).not_to be_success + expect(result.error.code).to eq("56") + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Response indicating the Status with ID `56`, and description `Property does not exist.`" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + + it "returns an empty array if property have no seasons seasons" do + stub_data = read_fixture("rentals_united/seasons/no_seasons.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_seasons + expect(result).to be_success + expect(result.value).to eq([]) + end + + context "when response contains seasons data" do + let(:file_name) { "rentals_united/seasons/success.xml" } + + before do + stub_data = read_fixture(file_name) + stub_call(:post, url) { [200, {}, stub_data] } + end + + it "returns seasons" do + result = subject.fetch_seasons + expect(result).to be_success + expect(result.value.size).to eq(2) + expect(result.value).to all(be_kind_of(RentalsUnited::Entities::Season)) + end + end + + context "when response from the api is not well-formed xml" do + it "returns a result with an appropriate error" do + stub_data = read_fixture("rentals_united/bad_xml.xml") + stub_call(:post, url) { [200, {}, stub_data] } + + result = subject.fetch_seasons + + expect(result).not_to be_success + expect(result.error.code).to eq(:unrecognised_response) + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq( + "Error response could not be recognised (no `Status` tag in the response)" + ) + expect(event[:backtrace]).to be_kind_of(Array) + expect(event[:backtrace].any?).to be true + end + end + + context "when request fails due to timeout error" do + it "returns a result with an appropriate error" do + stub_call(:post, url) { raise Faraday::TimeoutError } + + result = subject.fetch_seasons + + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + + event = Concierge.context.events.last.to_h + expect(event[:message]).to eq("timeout") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/converters/country_code_spec.rb b/spec/lib/concierge/suppliers/rentals_united/converters/country_code_spec.rb new file mode 100644 index 000000000..627faf749 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/converters/country_code_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Converters::CountryCode do + it "returns country code by its name" do + code = described_class.code_by_name("Kuwait") + expect(code).to eq("KW") + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/dictionaries/amenities_spec.rb b/spec/lib/concierge/suppliers/rentals_united/dictionaries/amenities_spec.rb new file mode 100644 index 000000000..3bdf05628 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/dictionaries/amenities_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Dictionaries::Amenities do + describe "#supported_amenities" do + it "returns array of supported amenities" do + expect(described_class.supported_amenities).to be_kind_of(Array) + expect(described_class.supported_amenities).to all(be_kind_of(Hash)) + end + end + + describe "#convert" do + it "returns an emptry string when there is no given RU services" do + service_ids = [] + + amenities = described_class.new(service_ids).convert + expect(amenities).to eq([]) + end + + it "returns an empty string when there is no any match in given RU services" do + service_ids = ["8888", "9999"] + + amenities = described_class.new(service_ids).convert + expect(amenities).to eq([]) + end + + it "converts RU services to amenities if there is a match" do + service_ids = ["11", "74"] + + amenities = described_class.new(service_ids).convert + expect(amenities).to eq(["laundry", "tv"]) + end + + it "skips unknown RU services and returns only amenities with matches" do + service_ids = ["11", "74", "8888"] + + amenities = described_class.new(service_ids).convert + expect(amenities).to eq(["laundry", "tv"]) + end + + it "removes duplicates if there are two RU services with the same name" do + service_ids = ["89", "89", "89"] + + amenities = described_class.new(service_ids).convert + expect(amenities).to eq(["balcony"]) + end + + it "removes duplicates if there are RU services with the same match" do + service_ids = ["89", "96"] + + amenities = described_class.new(service_ids).convert + expect(amenities).to eq(["balcony"]) + end + end + + describe "#smoking_allowed?" do + let(:service_ids) { ["89", "96"] } + + it "returns false when no smoking_allowed facilities are included" do + smoking_allowed_ids = [] + ids = (service_ids + smoking_allowed_ids).flatten + + dictionary = described_class.new(ids) + expect(dictionary).not_to be_smoking_allowed + end + + it "returns true when one of smoking_allowed facilities is included" do + smoking_allowed_ids = ["799", "802"] + smoking_allowed_ids.each do |id| + ids = (service_ids + [id]).flatten + + dictionary = described_class.new(ids) + expect(dictionary).to be_smoking_allowed + end + end + end + + describe "#pets_allowed?" do + let(:service_ids) { ["89", "96"] } + + it "returns false when no pets_allowed facilities are included" do + pets_allowed_ids = [] + ids = (service_ids + pets_allowed_ids).flatten + + dictionary = described_class.new(ids) + expect(dictionary).not_to be_pets_allowed + end + + it "returns true when one of pets_allowed facilities is included" do + pets_allowed_ids = ["595"] + pets_allowed_ids.each do |id| + ids = (service_ids + [id]).flatten + + dictionary = described_class.new(ids) + expect(dictionary).to be_pets_allowed + end + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms_spec.rb b/spec/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms_spec.rb new file mode 100644 index 000000000..d5bbe1131 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/dictionaries/bedrooms_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Dictionaries::Bedrooms do + describe "#count_by_type_id" do + it "returns bedrooms count by type id" do + count = described_class.count_by_type_id("46") + + expect(count).to eq(23) + end + + it "returns nil if property type was not found" do + count = described_class.count_by_type_id("9999") + + expect(count).to eq(nil) + end + + it "returns 0 for Studio" do + count = described_class.count_by_type_id("1") + + expect(count).to eq(0) + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/dictionaries/property_types_spec.rb b/spec/lib/concierge/suppliers/rentals_united/dictionaries/property_types_spec.rb new file mode 100644 index 000000000..5ac3a8cfd --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/dictionaries/property_types_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Dictionaries::PropertyTypes do + describe "#find" do + it "returns property type by its id" do + property_type = described_class.find("35") + + expect(property_type).to be_kind_of( + RentalsUnited::Entities::PropertyType + ) + expect(property_type.id).to eq("35") + expect(property_type.name).to eq("Villa") + expect(property_type.roomorama_name).to eq("house") + expect(property_type.roomorama_subtype_name).to eq("villa") + end + + it "returns nil if property type was not found" do + property_type = described_class.find("3500") + + expect(property_type).to be_nil + end + + it "returns nil if property type was found but has no mapping to Roomorama" do + property_type = described_class.find("20") + + expect(property_type).to be_nil + end + end + + describe "#all" do + it "returns all property types" do + property_types = described_class.all + + expect(property_types.size).to eq(12) + expect(property_types).to all( + be_kind_of(RentalsUnited::Entities::PropertyType) + ) + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/dictionaries/statuses_spec.rb b/spec/lib/concierge/suppliers/rentals_united/dictionaries/statuses_spec.rb new file mode 100644 index 000000000..8e815f70c --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/dictionaries/statuses_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Dictionaries::Statuses do + let(:supported_error_codes) { 0..130 } + + it "finds error description" do + message = described_class.find("118") + expect(message).to eq("Max number of guests must be of positive value.") + end + + it "finds descriptions for all supported error codes" do + supported_error_codes.each do |error_code| + expect(described_class.find(error_code.to_s)).not_to be_nil + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/entities/properties_collection_spec.rb b/spec/lib/concierge/suppliers/rentals_united/entities/properties_collection_spec.rb new file mode 100644 index 000000000..a17cbb8ab --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/entities/properties_collection_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Entities::PropertiesCollection do + let(:entries) do + [ + { property_id: '10', location_id: '100' }, + { property_id: '20', location_id: '200' } + ] + end + + let(:collection) { described_class.new(entries) } + let(:empty_collection) { described_class.new([]) } + + describe "size" do + it "returns size of collection" do + expect(collection.size).to eq(2) + end + + it "returns size of empty collection" do + expect(empty_collection.size).to eq(0) + end + end + + describe "#property_ids" do + it "returns array with property ids of entries in collection" do + expect(collection.property_ids).to eq(["10", "20"]) + end + + it "returns array with property ids of entries in empty collection" do + expect(empty_collection.property_ids).to eq([]) + end + end + + describe "#location_ids" do + it "returns array with location ids of entries in collection" do + expect(collection.location_ids).to eq(["100", "200"]) + end + + it "returns array with location ids of entries in empty collection" do + expect(empty_collection.location_ids).to eq([]) + end + + context "with duplicate locations" do + let(:entries) do + [ + { property_id: '10', location_id: '100' }, + { property_id: '20', location_id: '100' } + ] + end + + it "returns array with uniq location ids of entries in collection" do + expect(collection.location_ids).to eq(["100"]) + end + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/entities/season_spec.rb b/spec/lib/concierge/suppliers/rentals_united/entities/season_spec.rb new file mode 100644 index 000000000..8065d3cda --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/entities/season_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Entities::Season do + let(:attributes) do + { + date_from: Date.parse("2016-09-01"), + date_to: Date.parse("2016-09-15"), + price: 200.00 + } + end + + let(:season) { Entities::Season.new(attributes) } + + describe "has_price_for_date?" do + it "returns true when given date is between from and to dates" do + date = Date.parse("2016-09-04") + expect(season.has_price_for_date?(date)).to eq(true) + end + + it "returns true when given date matches to date_from" do + date = Date.parse("2016-09-01") + expect(season.has_price_for_date?(date)).to eq(true) + end + + it "returns true when given date matches to date_to" do + date = Date.parse("2016-09-15") + expect(season.has_price_for_date?(date)).to eq(true) + end + + it "returns false when given date is less than date_from" do + date = Date.parse("2016-08-15") + expect(season.has_price_for_date?(date)).to eq(false) + end + + it "returns false when given date is greater than date_to" do + date = Date.parse("2016-09-16") + expect(season.has_price_for_date?(date)).to eq(false) + end + end + + describe "number_of_days" do + it "returns number of days in specified date range" do + expect(season.number_of_days).to eq(15) + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/importer_spec.rb b/spec/lib/concierge/suppliers/rentals_united/importer_spec.rb new file mode 100644 index 000000000..45c048452 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/importer_spec.rb @@ -0,0 +1,88 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Importer do + include Support::HTTPStubbing + include Support::Fixtures + + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:importer) { described_class.new(credentials) } + + describe "#fetch_properties_collection_for_owner" do + let(:owner_id) { "588788" } + + it "calls fetcher class to load properties collection" do + fetcher_class = RentalsUnited::Commands::PropertiesCollectionFetcher + + expect_any_instance_of(fetcher_class). + to(receive(:fetch_properties_collection_for_owner)) + importer.fetch_properties_collection_for_owner(owner_id) + end + end + + describe "#fetch_locations" do + let(:location_ids) { ['10', '20', '30'] } + + it "calls fetcher class to load locations" do + fetcher_class = RentalsUnited::Commands::LocationsFetcher + + expect_any_instance_of(fetcher_class).to(receive(:fetch_locations)) + importer.fetch_locations(location_ids) + end + end + + describe "#fetch_location_currencies" do + it "calls fetcher class to load locations and currencies" do + fetcher_class = RentalsUnited::Commands::LocationCurrenciesFetcher + + expect_any_instance_of(fetcher_class).to( + receive(:fetch_location_currencies) + ) + importer.fetch_location_currencies + end + end + + describe "#fetch_property" do + let(:property_id) { "588788" } + + it "calls fetcher class to load property" do + fetcher_class = RentalsUnited::Commands::PropertyFetcher + + expect_any_instance_of(fetcher_class).to(receive(:fetch_property)) + importer.fetch_property(property_id) + end + end + + describe "#fetch_owner" do + let(:owner_id) { "123" } + + it "calls fetcher class to load owners" do + fetcher_class = RentalsUnited::Commands::OwnerFetcher + + expect_any_instance_of(fetcher_class).to(receive(:fetch_owner)) + importer.fetch_owner(owner_id) + end + end + + describe "#fetch_availabilities" do + let(:property_id) { "588788" } + + it "calls fetcher class to load availabilities" do + fetcher_class = RentalsUnited::Commands::AvailabilitiesFetcher + + expect_any_instance_of(fetcher_class).to(receive(:fetch_availabilities)) + importer.fetch_availabilities(property_id) + end + end + + describe "#fetch_seasons" do + let(:property_id) { "588788" } + + it "calls fetcher class to load availabilities" do + fetcher_class = RentalsUnited::Commands::SeasonsFetcher + + expect_any_instance_of(fetcher_class).to(receive(:fetch_seasons)) + importer.fetch_seasons(property_id) + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/availability_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/availability_spec.rb new file mode 100644 index 000000000..6dec31389 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/availability_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Mappers::Availability do + let(:availability_hash) do + { + "IsBlocked"=>true, + "MinStay"=>"1", + "Changeover"=>"4", + "@Date"=>"2016-09-07" + } + end + + it "builds availability object" do + mapper = described_class.new(availability_hash) + availability = mapper.build_availability + + expect(availability).to be_kind_of(RentalsUnited::Entities::Availability) + expect(availability.date).to be_kind_of(Date) + expect(availability.date.to_s).to eq("2016-09-07") + expect(availability.available).to eq(false) + expect(availability.minimum_stay).to eq(1) + expect(availability.changeover).to eq(4) + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/calendar_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/calendar_spec.rb new file mode 100644 index 000000000..3f50cfe0f --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/calendar_spec.rb @@ -0,0 +1,200 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Mappers::Calendar do + let(:property_id) { "1234" } + let(:seasons) do + [ + RentalsUnited::Entities::Season.new( + date_from: Date.parse("2016-09-01"), + date_to: Date.parse("2016-09-15"), + price: 150.0 + ), + RentalsUnited::Entities::Season.new( + date_from: Date.parse("2016-10-01"), + date_to: Date.parse("2016-10-15"), + price: 200.0 + ) + ] + end + + let(:availabilities) do + [ + RentalsUnited::Entities::Availability.new( + date: Date.parse("2016-09-01"), + available: false, + minimum_stay: 2, + changeover: 4 + ), + RentalsUnited::Entities::Availability.new( + date: Date.parse("2016-10-14"), + available: true, + minimum_stay: 1, + changeover: 4 + ) + ] + end + + it "builds empty property calendar" do + mapper = described_class.new(property_id, [], []) + result = mapper.build_calendar + expect(result).to be_kind_of(Result) + expect(result).to be_success + + calendar = result.value + expect(calendar).to be_kind_of(Roomorama::Calendar) + expect(calendar.identifier).to eq(property_id) + expect(calendar.entries).to eq([]) + end + + it "builds calendar with entries" do + mapper = described_class.new(property_id, seasons, availabilities) + result = mapper.build_calendar + expect(result).to be_kind_of(Result) + expect(result).to be_success + + calendar = result.value + expect(calendar.validate!).to eq(true) + + expect(calendar).to be_kind_of(Roomorama::Calendar) + expect(calendar.identifier).to eq(property_id) + expect(calendar.entries.size).to eq(2) + expect(calendar.entries).to all(be_kind_of(Roomorama::Calendar::Entry)) + + sep_entry = calendar.entries.find { |e| e.date.to_s == "2016-09-01" } + expect(sep_entry.available).to eq(false) + expect(sep_entry.minimum_stay).to eq(2) + expect(sep_entry.nightly_rate).to eq(150.0) + expect(sep_entry.checkin_allowed).to eq(true) + expect(sep_entry.checkout_allowed).to eq(true) + + oct_entry = calendar.entries.find { |e| e.date.to_s == "2016-10-14" } + expect(oct_entry.available).to eq(true) + expect(oct_entry.minimum_stay).to eq(1) + expect(oct_entry.nightly_rate).to eq(200.0) + expect(sep_entry.checkin_allowed).to eq(true) + expect(sep_entry.checkout_allowed).to eq(true) + end + + it "keeps even not valid calendar entries setting nightly_rate to 0" do + availabilities << RentalsUnited::Entities::Availability.new( + date: Date.parse("2017-01-01"), + available: true, + minimum_stay: 5, + changeover: 4 + ) + mapper = described_class.new(property_id, seasons, availabilities) + result = mapper.build_calendar + expect(result).to be_kind_of(Result) + expect(result).to be_success + + calendar = result.value + + entry = calendar.entries.find { |e| e.date.to_s == "2017-01-01" } + expect(entry.available).to eq(false) + expect(entry.minimum_stay).to eq(5) + expect(entry.nightly_rate).to eq(0.0) + expect(entry.checkin_allowed).to eq(false) + expect(entry.checkout_allowed).to eq(false) + + expect(calendar.validate!).to eq(true) + end + + context "while availabilities changeover mapping" do + let(:date) { Date.parse("2016-09-01") } + let(:availabilities) do + [ + RentalsUnited::Entities::Availability.new( + date: date, + available: false, + minimum_stay: 2, + changeover: changeover + ) + ] + end + + context "when changeover type id is 1" do + let(:changeover) { 1 } + + it "sets checkin to be allowed and checkout to be denied" do + mapper = described_class.new(property_id, seasons, availabilities) + result = mapper.build_calendar + expect(result).to be_kind_of(Result) + expect(result).to be_success + + calendar = result.value + expect(calendar.validate!).to eq(true) + + entry = calendar.entries.find { |e| e.date.to_s == date.to_s } + expect(entry.checkin_allowed).to eq(true) + expect(entry.checkout_allowed).to eq(false) + end + end + + context "when changeover type id is 2" do + let(:changeover) { 2 } + + it "sets checkin to be denied and checkout to be allowed" do + mapper = described_class.new(property_id, seasons, availabilities) + result = mapper.build_calendar + expect(result).to be_kind_of(Result) + expect(result).to be_success + + calendar = result.value + expect(calendar.validate!).to eq(true) + + entry = calendar.entries.find { |e| e.date.to_s == date.to_s } + expect(entry.checkin_allowed).to eq(false) + expect(entry.checkout_allowed).to eq(true) + end + end + + context "when changeover type id is 3" do + let(:changeover) { 3 } + + it "sets both checkin and checkout to be denied" do + mapper = described_class.new(property_id, seasons, availabilities) + result = mapper.build_calendar + expect(result).to be_kind_of(Result) + expect(result).to be_success + + calendar = result.value + expect(calendar.validate!).to eq(true) + + entry = calendar.entries.find { |e| e.date.to_s == date.to_s } + expect(entry.checkin_allowed).to eq(false) + expect(entry.checkout_allowed).to eq(false) + end + end + + context "when changeover type id is 4" do + let(:changeover) { 4 } + + it "sets both checkin and checkout to be allowed" do + mapper = described_class.new(property_id, seasons, availabilities) + result = mapper.build_calendar + expect(result).to be_kind_of(Result) + expect(result).to be_success + + calendar = result.value + expect(calendar.validate!).to eq(true) + + entry = calendar.entries.find { |e| e.date.to_s == date.to_s } + expect(entry.checkin_allowed).to eq(true) + expect(entry.checkout_allowed).to eq(true) + end + end + + context "when changeover type id is unknown" do + let(:changeover) { 5 } + + it "returns not supported changeover error" do + mapper = described_class.new(property_id, seasons, availabilities) + result = mapper.build_calendar + expect(result).not_to be_success + expect(result.error.code).to eq(:not_supported_changeover) + end + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/owners_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/owners_spec.rb new file mode 100644 index 000000000..fa329be5a --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/owners_spec.rb @@ -0,0 +1,27 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Mappers::Owner do + let(:owner_hash) do + { + "FirstName" => "Foo", + "SurName" => "Bar", + "CompanyName" => "RU Test", + "Email" => "foobar@gmail.com", + "Phone" => "519461272", + "@OwnerID" => "419680" + } + end + + let(:safe_hash) { Concierge::SafeAccessHash.new(owner_hash) } + let(:subject) { described_class.new(safe_hash) } + + it "builds owner object" do + owner = subject.build_owner + expect(owner).to be_kind_of(RentalsUnited::Entities::Owner) + expect(owner.id).to eq("419680") + expect(owner.first_name).to eq("Foo") + expect(owner.last_name).to eq("Bar") + expect(owner.email).to eq("foobar@gmail.com") + expect(owner.phone).to eq("519461272") + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/price_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/price_spec.rb new file mode 100644 index 000000000..d73e5b0be --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/price_spec.rb @@ -0,0 +1,28 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Mappers::Price do + context "when price exists" do + let(:value) { 123.50 } + let(:subject) { described_class.new(value) } + + it "builds price object" do + price = subject.build_price + expect(price).to be_kind_of(RentalsUnited::Entities::Price) + expect(price.total).to eq(value) + expect(price.available?).to be true + end + end + + context "when price does not exist" do + [nil, ""].each do |value| + let(:subject) { described_class.new(value) } + + it "builds price object" do + price = subject.build_price + expect(price).to be_kind_of(RentalsUnited::Entities::Price) + expect(price.total).to eq(0.0) + expect(price.available?).to be false + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/property_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/property_spec.rb new file mode 100644 index 000000000..9a9b21e90 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/property_spec.rb @@ -0,0 +1,210 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Mappers::Property do + include Support::Fixtures + + let(:file_name) { "rentals_united/properties/property.xml" } + let(:property_hash) do + stub_data = read_fixture(file_name) + safe_hash = RentalsUnited::ResponseParser.new.to_hash(stub_data) + safe_hash.get("Pull_ListSpecProp_RS.Property") + end + + let(:subject) { described_class.new(property_hash) } + let(:property) { subject.build_property } + + context "when hash contains property data" do + it "builds property object" do + expect(property).to be_kind_of(RentalsUnited::Entities::Property) + end + + it "sets id to the property" do + expect(property.id).to eq("519688") + end + + it "sets title to the property" do + expect(property.title).to eq("Test property") + end + + it "sets property_type_id to the property" do + expect(property.property_type_id).to eq("35") + end + + it "sets max_guests to the property" do + expect(property.max_guests).to eq(2) + end + + it "sets bedroom_type_id to the property" do + expect(property.bedroom_type_id).to eq("4") + end + + it "sets surface to the property" do + expect(property.surface).to eq(39) + end + + it "sets address information to the property" do + expect(property.lat).to eq(55.0003426) + expect(property.lng).to eq(73.2965942999999) + expect(property.address).to eq("Test street address") + expect(property.postal_code).to eq("644119") + end + + it "sets en description to the property" do + expect(property.description).to eq("Test description") + end + + it "sets check_in_time to the property" do + expect(property.check_in_time).to eq("13:00-17:00") + end + + it "sets check_out_time to the property" do + expect(property.check_out_time).to eq("11:00") + end + + it "sets active flag to the property" do + expect(property.active?).to eq(true) + end + + it "sets archived flag to the property" do + expect(property.archived?).to eq(false) + end + + it "sets owner_id to the property" do + expect(property.owner_id).to eq("427698") + end + + it "sets security_deposit_type to the property" do + expect(property.security_deposit_type).to eq("5") + end + + it "sets security_deposit_amount to the property" do + expect(property.security_deposit_amount).to eq(5.50) + end + + context "when property is not active" do + let(:file_name) { "rentals_united/properties/not_active.xml" } + + it "returns not active property" do + expect(property.active?).to eq(false) + end + end + + context "when property is archived" do + let(:file_name) { "rentals_united/properties/archived.xml" } + + it "returnes archived property" do + expect(property.archived?).to eq(true) + end + end + end + + context "when multiple descriptions are available" do + let(:file_name) { "rentals_united/properties/property_with_multiple_descriptions.xml" } + + it "sets en description to the property" do + expect(property.description).to eq("Yet another one description") + end + end + + context "when no available descriptions" do + let(:file_name) { "rentals_united/properties/property_without_descriptions.xml" } + + it "sets description to nil" do + expect(property.description).to be_nil + end + end + + context "when mapping single image to property" do + let(:file_name) { "rentals_united/properties/property_with_one_image.xml" } + + let(:expected_images) do + { + "a0c68acc113db3b58376155c283dfd59" => { + url: "https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082399089701851.jpg", + caption: 'Main image' + } + } + end + + it "adds array of with one image to the property" do + expect(property.images.size).to eq(1) + expect(property.images).to all(be_kind_of(Roomorama::Image)) + expect(property.images.map(&:identifier)).to eq(expected_images.keys) + + property.images.each do |image| + expect(image.url).to eq(expected_images[image.identifier][:url]) + expect(image.caption).to eq(expected_images[image.identifier][:caption]) + end + end + end + + context "when mapping multiple images to property" do + let(:expected_images) do + { + "62fc304eb20a25669b84d2ca2ea61308" => { + url: "https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082398988145159.jpg", + caption: 'Interior' + }, + "a0c68acc113db3b58376155c283dfd59" => { + url: "https://dwe6atvmvow8k.cloudfront.net/ru/427698/519688/636082399089701851.jpg", + caption: 'Main image' + } + } + end + + it "adds array of images to the property" do + expect(property.images.size).to eq(2) + expect(property.images).to all(be_kind_of(Roomorama::Image)) + expect(property.images.map(&:identifier)).to eq(expected_images.keys) + + property.images.each do |image| + expect(image.url).to eq(expected_images[image.identifier][:url]) + expect(image.caption).to eq(expected_images[image.identifier][:caption]) + end + end + end + + context "when there is no images for property" do + let(:file_name) { "rentals_united/properties/property_without_images.xml" } + + it "returns an empty array for property images" do + expect(property.images).to eq([]) + end + end + + context "when there is existing amenities for property" do + let(:file_name) { "rentals_united/properties/property.xml" } + + it "returns an array with property amenities" do + expect(property.amenities).to eq( + ["7", "100", "180", "187", "227", "281", "368", "596", "689", "802", "803"] + ) + end + end + + context "when there is no amenities for property" do + let(:file_name) { "rentals_united/properties/property_without_amenities.xml" } + + it "returns an empty array for property amenities" do + expect(property.amenities).to eq([]) + end + end + + context "when mapping floors" do + context "when it's usual floor" do + let(:file_name) { "rentals_united/properties/property.xml" } + + it "sets floor to the property" do + expect(property.floor).to eq(3) + end + end + + context "when it's Basement encoded by -1000" do + let(:file_name) { "rentals_united/properties/basement_floor.xml" } + + it "sets floor -1 to the property" do + expect(property.floor).to eq(-1) + end + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/quotation_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/quotation_spec.rb new file mode 100644 index 000000000..6472364a3 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/quotation_spec.rb @@ -0,0 +1,40 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Mappers::Quotation do + include Support::Factories + + context "when price exists" do + let!(:host) { create_host(fee_percentage: 7.0) } + let!(:property) { create_property(identifier: "567", host_id: host.id) } + let(:price) { RentalsUnited::Entities::Price.new(total: 123.45, available: true) } + let(:currency) { "USD" } + let(:quotation_params) do + API::Controllers::Params::Quote.new( + property_id: property.identifier, + check_in: "2016-09-19", + check_out: "2016-09-20", + guests: 3 + ) + end + let(:subject) do + described_class.new( + price, + currency, + quotation_params + ) + end + + it "builds quotation object" do + quotation = subject.build_quotation + expect(quotation).to be_kind_of(Quotation) + expect(quotation.property_id).to eq(quotation_params[:property_id]) + expect(quotation.check_in).to eq(quotation_params[:check_in]) + expect(quotation.check_out).to eq(quotation_params[:check_out]) + expect(quotation.guests).to eq(quotation_params[:guests]) + expect(quotation.total).to eq(price.total) + expect(quotation.available).to eq(price.available?) + expect(quotation.currency).to eq(currency) + expect(quotation.host_fee_percentage).to eq(7.0) + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/roomorama_property_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/roomorama_property_spec.rb new file mode 100644 index 000000000..917b732b2 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/roomorama_property_spec.rb @@ -0,0 +1,375 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::Mappers::RoomoramaProperty do + include Support::Fixtures + + let(:ru_property_hash) do + { + id: "519688", + title: "Test Property", + lat: 55.0003426, + lng: 73.2965942999999, + address: "Test street address", + postal_code: "644119", + max_guests: 2, + bedroom_type_id: "4", + property_type_id: "35", + active: true, + archived: false, + surface: 39, + owner_id: "378000", + security_deposit_type: "5", + security_deposit_amount: 5.50, + check_in_time: "13:00-17:00", + check_out_time: "11:00", + floor: 5, + description: "Test Description", + images: [], + amenities: [] + } + end + + let(:ru_property) { RentalsUnited::Entities::Property.new(ru_property_hash) } + + let(:location) do + double( + id: '1505', + city: 'Paris', + neighborhood: 'Ile-de-France', + country: 'France', + currency: 'EUR' + ) + end + + let(:owner) do + double( + id: '478000', + first_name: 'John', + last_name: 'Doe', + email: 'john.doe@gmail.com', + phone: '3128329138' + ) + end + + let(:seasons) do + [ + RentalsUnited::Entities::Season.new( + date_from: Date.parse("2016-09-01"), + date_to: Date.parse("2016-09-30"), + price: 200.00 + ), + RentalsUnited::Entities::Season.new( + date_from: Date.parse("2016-10-01"), + date_to: Date.parse("2016-10-02"), + price: 1000.00 + ), + ] + end + + let(:subject) { described_class.new(ru_property, location, owner, seasons) } + let(:result) { subject.build_roomorama_property } + let(:property) { result.value } + + it "builds property object" do + expect(property).to be_kind_of(Roomorama::Property) + end + + it "sets id to the property" do + expect(property.identifier).to eq(ru_property.id) + end + + it "sets title to the property" do + expect(property.title).to eq(ru_property.title) + end + + it "sets description to the property" do + expect(property.description).to eq(ru_property.description) + end + + it "sets type to the property" do + expect(property.type).to eq("house") + end + + it "sets subtype to the property" do + expect(property.subtype).to eq("villa") + end + + it "sets max_guests to the property" do + expect(property.max_guests).to eq(ru_property.max_guests) + end + + it "sets number_of_bedrooms to the property" do + expect(property.number_of_bedrooms).to eq(3) + end + + it "sets floor to the property" do + expect(property.floor).to eq(5) + end + + it "sets surface to the property" do + expect(property.surface).to eq(ru_property.surface) + end + + it "sets surface_unit to the property" do + expect(property.surface_unit).to eq("metric") + end + + it "sets address information to the property" do + expect(property.lat).to eq(55.0003426) + expect(property.lng).to eq(73.2965942999999) + expect(property.address).to eq("Test street address") + expect(property.city).to eq("Paris") + expect(property.neighborhood).to eq("Ile-de-France") + expect(property.postal_code).to eq("644119") + expect(property.country_code).to eq("FR") + end + + it "sets check_in_time to the property" do + expect(property.check_in_time).to eq("13:00-17:00") + end + + it "sets check_out_time to the property" do + expect(property.check_out_time).to eq("11:00") + end + + it "sets currency to the property" do + expect(property.currency).to eq("EUR") + end + + it "sets cancellation_policy to the property" do + expect(property.cancellation_policy).to eq("super_elite") + end + + it "sets default_to_available flag to false" do + expect(property.default_to_available).to eq(false) + end + + it "does not set multi-unit flag" do + expect(property.multi_unit).to be_nil + end + + it "sets instant_booking flag" do + expect(property.instant_booking?).to eq(true) + end + + it "sets owner name" do + expect(property.owner_name).to eq("John Doe") + end + + it "sets owner email" do + expect(property.owner_email).to eq("john.doe@gmail.com") + end + + it "sets owner phone number" do + expect(property.owner_phone_number).to eq("3128329138") + end + + it "sets cleaning fields" do + expect(property.services_cleaning).to be false + expect(property.services_cleaning_required).to be nil + expect(property.services_cleaning_rate).to be nil + end + + it "sets security_deposit_amount" do + expect(property.security_deposit_amount).to eq(5.5) + end + + it "sets security_deposit_currency_code" do + expect(property.security_deposit_currency_code).to eq("EUR") + end + + it "sets security_deposit_currency_code" do + expect(property.security_deposit_type).to eq("cash") + end + + it "sets property rates" do + expect(property.nightly_rate).to eq(250.0) + expect(property.weekly_rate).to eq(1750.0) + expect(property.monthly_rate).to eq(7500.0) + end + + context "when given avg price is not integer" do + let(:seasons) do + [ + RentalsUnited::Entities::Season.new( + date_from: Date.parse("2016-09-01"), + date_to: Date.parse("2016-09-04"), + price: 199.99 + ), + RentalsUnited::Entities::Season.new( + date_from: Date.parse("2016-10-01"), + date_to: Date.parse("2016-10-13"), + price: 1000.00 + ), + ] + end + + it "sets rounded property rates" do + expect(property.nightly_rate).to eq(811.76) + expect(property.weekly_rate).to eq(5682.34) + expect(property.monthly_rate).to eq(24352.87) + end + end + + context "when property has no security_deposit" do + it "sets security_deposit_amount to nil" do + ru_property_hash[:security_deposit_type] = "1" + expect(property.security_deposit_amount).to be_nil + end + + it "sets security_deposit_currency_code to nil" do + ru_property_hash[:security_deposit_type] = "1" + expect(property.security_deposit_currency_code).to be_nil + end + + it "sets security_deposit_type to nil" do + ru_property_hash[:security_deposit_type] = "1" + expect(property.security_deposit_type).to be_nil + end + end + + context "when property has not supported security_deposit" do + ["2", "3", "4"].each do |type_id| + it "returns security_deposit_error" do + ru_property_hash[:security_deposit_type] = type_id + expect(result).not_to be_success + expect(result.error.code).to eq(:security_deposit_not_supported) + end + end + end + + context "when property is archived" do + it "returns error" do + ru_property_hash[:archived] = true + + expect(result).not_to be_success + expect(result.error.code).to eq(:attempt_to_build_archived_property) + end + end + + context "when property is not active" do + it "returns error" do + ru_property_hash[:active] = false + + expect(result).not_to be_success + expect(result.error.code).to eq(:attempt_to_build_not_active_property) + end + end + + context "when property is hotel-typed" do + it "returns error" do + ru_property_hash[:property_type_id] = 20 + + expect(result).not_to be_success + expect(result.error.code).to eq(:property_type_not_supported) + end + end + + context "when property is boat-typed" do + it "returns error" do + ru_property_hash[:property_type_id] = 64 + + expect(result).not_to be_success + expect(result.error.code).to eq(:property_type_not_supported) + end + end + + context "when property is camping-typed" do + it "returns error" do + ru_property_hash[:property_type_id] = 66 + + expect(result).not_to be_success + expect(result.error.code).to eq(:property_type_not_supported) + end + end + + context "when mapping amenities" do + let(:amenities) do + ["7", "100", "180", "187", "227", "281", "368", "596", "689", "802", "803"] + end + + it "adds amenities to property" do + ru_property_hash[:amenities] = amenities + + expect(property.amenities).to eq( + ["bed_linen_and_towels", "airconditioning", "pool", "wheelchairaccess", "elevator", "parking"] + ) + end + + it "sets smoking_allowed to false" do + amenities_dict = RentalsUnited::Dictionaries::Amenities + + expect_any_instance_of(amenities_dict) + .to(receive(:smoking_allowed?)) + .and_return(false) + + + expect(property.smoking_allowed).to eq(false) + end + + it "sets smoking_allowed to true" do + amenities_dict = RentalsUnited::Dictionaries::Amenities + + expect_any_instance_of(amenities_dict) + .to(receive(:smoking_allowed?)) + .and_return(true) + + expect(property.smoking_allowed).to eq(true) + end + + it "sets pets_allowed to false" do + amenities_dict = RentalsUnited::Dictionaries::Amenities + + expect_any_instance_of(amenities_dict) + .to(receive(:pets_allowed?)) + .and_return(false) + + expect(property.pets_allowed).to eq(false) + end + + it "sets pets_allowed to true" do + amenities_dict = RentalsUnited::Dictionaries::Amenities + + expect_any_instance_of(amenities_dict) + .to(receive(:pets_allowed?)) + .and_return(true) + + expect(property.pets_allowed).to eq(true) + end + end + + context "when there is no amenities" do + let(:amenities) { [] } + + it "keeps amenities empty" do + expect(property.amenities).to eq([]) + end + end + + context "when mapping images" do + let(:image) do + image = Roomorama::Image.new("1") + image.url = "http://url.com/123.jpg" + image.caption = "house" + image + end + + let(:images) do + [image] + end + + it "adds images to property" do + ru_property_hash[:images] = images + + expect(property.images).to eq(images) + end + end + + context "when there is no images" do + it "keeps amenities empty" do + ru_property_hash[:images] = [] + + expect(property.images).to eq([]) + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/mappers/season_spec.rb b/spec/lib/concierge/suppliers/rentals_united/mappers/season_spec.rb new file mode 100644 index 000000000..2ed380408 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/mappers/season_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +module RentalsUnited + RSpec.describe Mappers::Season do + let(:season_hash) do + { + "Price"=>"200.0000", + "Extra"=>"10.0000", + "@DateFrom"=>"2016-09-07", + "@DateTo"=>"2016-09-30" + } + end + + it "builds season object" do + mapper = described_class.new(season_hash) + season = mapper.build_season + + expect(season).to be_kind_of(RentalsUnited::Entities::Season) + expect(season.date_from).to be_kind_of(Date) + expect(season.date_from.to_s).to eq("2016-09-07") + expect(season.date_to).to be_kind_of(Date) + expect(season.date_to.to_s).to eq("2016-09-30") + expect(season.price).to eq(200.0) + end + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/payload_builder_spec.rb b/spec/lib/concierge/suppliers/rentals_united/payload_builder_spec.rb new file mode 100644 index 000000000..b320b7a98 --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/payload_builder_spec.rb @@ -0,0 +1,386 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::PayloadBuilder do + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:builder) { described_class.new(credentials) } + + describe '#build_properties_collection_fetch_payload' do + let(:params) do + { + owner_id: "123" + } + end + + it 'embedds username and password to request' do + xml = builder.build_properties_collection_fetch_payload( + params[:owner_id] + ) + hash = to_hash(xml) + + authentication = hash.get("Pull_ListOwnerProp_RQ.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds owner_id request' do + xml = builder.build_properties_collection_fetch_payload( + params[:owner_id] + ) + hash = to_hash(xml) + + owner_id = hash.get("Pull_ListOwnerProp_RQ.OwnerID") + expect(owner_id).to eq(params[:owner_id]) + end + + it 'adds include_nla flag to request' do + xml = builder.build_properties_collection_fetch_payload( + params[:owner_id] + ) + hash = to_hash(xml) + + include_nla = hash.get("Pull_ListOwnerProp_RQ.IncludeNLA") + expect(include_nla).to eq(false) + end + end + + describe '#build_owner_fetch_payload' do + let(:params) do + { + owner_id: "123" + } + end + + it 'embedds username and password to request' do + xml = builder.build_owner_fetch_payload(params[:owner_id]) + hash = to_hash(xml) + + authentication = hash.get("Pull_GetOwnerDetails_RQ.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds owner_id request' do + xml = builder.build_owner_fetch_payload(params[:owner_id]) + hash = to_hash(xml) + + owner_id = hash.get("Pull_GetOwnerDetails_RQ.OwnerID") + expect(owner_id).to eq(params[:owner_id]) + end + end + + describe '#build_locations_fetch_payload' do + it 'embedds username and password to request' do + xml = builder.build_locations_fetch_payload + hash = to_hash(xml) + + authentication = hash.get("Pull_ListLocations_RQ.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + end + + describe '#build_location_currencies_fetch_payload' do + it 'embedds username and password to request' do + xml = builder.build_location_currencies_fetch_payload + hash = to_hash(xml) + + authentication = hash.get("Pull_ListCurrenciesWithCities_RQ.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + end + + describe '#build_property_fetch_payload' do + let(:params) do + { + property_id: "123" + } + end + + it 'embedds username and password to request' do + xml = builder.build_property_fetch_payload(params[:property_id]) + hash = to_hash(xml) + + authentication = hash.get("Pull_ListSpecProp_RQ.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds property_id to request' do + xml = builder.build_property_fetch_payload(params[:property_id]) + hash = to_hash(xml) + + property_id = hash.get("Pull_ListSpecProp_RQ.PropertyID") + expect(property_id).to eq(params[:property_id]) + end + end + + describe '#build_availabilities_fetch_payload' do + let(:params) do + { + property_id: "123", + date_from: "2016-09-01", + date_to: "2017-09-01" + } + end + + let(:root_tag) { "Pull_ListPropertyAvailabilityCalendar_RQ" } + + it 'embedds username and password to request' do + xml = builder.build_availabilities_fetch_payload( + params[:property_id], + params[:date_from], + params[:date_to] + ) + hash = to_hash(xml) + + authentication = hash.get("#{root_tag}.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds property_id to request' do + xml = builder.build_availabilities_fetch_payload( + params[:property_id], + params[:date_from], + params[:date_to] + ) + hash = to_hash(xml) + + property_id = hash.get("#{root_tag}.PropertyID") + expect(property_id).to eq(params[:property_id]) + end + + it 'adds date range to request' do + xml = builder.build_availabilities_fetch_payload( + params[:property_id], + params[:date_from], + params[:date_to] + ) + hash = to_hash(xml) + + date_from = hash.get("#{root_tag}.DateFrom") + date_to = hash.get("#{root_tag}.DateTo") + expect(date_from.to_s).to eq(params[:date_from]) + expect(date_to.to_s).to eq(params[:date_to]) + end + end + + describe '#build_seasons_fetch_payload' do + let(:params) do + { + property_id: "123", + date_from: "2016-09-01", + date_to: "2017-09-01" + } + end + + let(:root_tag) { "Pull_ListPropertyPrices_RQ" } + + it 'embedds username and password to request' do + xml = builder.build_seasons_fetch_payload( + params[:property_id], + params[:date_from], + params[:date_to] + ) + hash = to_hash(xml) + + authentication = hash.get("#{root_tag}.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds property_id to request' do + xml = builder.build_seasons_fetch_payload( + params[:property_id], + params[:date_from], + params[:date_to] + ) + hash = to_hash(xml) + + property_id = hash.get("#{root_tag}.PropertyID") + expect(property_id).to eq(params[:property_id]) + end + + it 'adds date range to request' do + xml = builder.build_seasons_fetch_payload( + params[:property_id], + params[:date_from], + params[:date_to] + ) + hash = to_hash(xml) + + date_from = hash.get("#{root_tag}.DateFrom") + date_to = hash.get("#{root_tag}.DateTo") + expect(date_from.to_s).to eq(params[:date_from]) + expect(date_to.to_s).to eq(params[:date_to]) + end + end + + describe '#build_price_fetch_payload' do + let(:stay_params) do + { + property_id: "123", + check_in: "2016-09-01", + check_out: "2016-09-02", + num_guests: 3 + } + end + + it 'embedds username and password to request' do + xml = builder.build_price_fetch_payload(stay_params) + hash = to_hash(xml) + + authentication = hash.get("Pull_GetPropertyAvbPrice_RQ.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds property_id to request' do + xml = builder.build_price_fetch_payload(stay_params) + hash = to_hash(xml) + + property_id = hash.get("Pull_GetPropertyAvbPrice_RQ.PropertyID") + expect(property_id).to eq(stay_params[:property_id]) + end + + it 'adds check in / check out dates to request' do + xml = builder.build_price_fetch_payload(stay_params) + hash = to_hash(xml) + + check_in = hash.get("Pull_GetPropertyAvbPrice_RQ.DateFrom").to_s + check_out = hash.get("Pull_GetPropertyAvbPrice_RQ.DateTo").to_s + expect(check_in).to eq(stay_params[:check_in]) + expect(check_out).to eq(stay_params[:check_out]) + end + + it 'adds num_guests to request' do + xml = builder.build_price_fetch_payload(stay_params) + hash = to_hash(xml) + + num_guests = hash.get("Pull_GetPropertyAvbPrice_RQ.NOP") + expect(num_guests).to eq(stay_params[:num_guests].to_s) + end + end + + describe '#build_booking_payload' do + let(:reservation_params) do + { + property_id: "123", + check_in: "2016-09-01", + check_out: "2016-09-02", + num_guests: 3, + total: "125.40", + user: { + first_name: "Test", + last_name: "User", + email: "testuser@example.com", + phone: "111-222-333", + address: "Address st 45", + postal_code: "98456" + } + } + end + + let(:root) { "Push_PutConfirmedReservationMulti_RQ" } + let(:reservation_root) { "#{root}.Reservation" } + let(:stay_info) { "#{reservation_root}.StayInfos.StayInfo" } + + it 'embedds username and password to request' do + xml = builder.build_booking_payload(reservation_params) + hash = to_hash(xml) + + authentication = hash.get("#{root}.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds property_id to request' do + xml = builder.build_booking_payload(reservation_params) + hash = to_hash(xml) + + property_id = hash.get("#{stay_info}.PropertyID") + expect(property_id).to eq(reservation_params[:property_id]) + end + + it 'adds check in / check out dates to request' do + xml = builder.build_booking_payload(reservation_params) + hash = to_hash(xml) + + check_in = hash.get("#{stay_info}.DateFrom").to_s + check_out = hash.get("#{stay_info}.DateTo").to_s + expect(check_in).to eq(reservation_params[:check_in]) + expect(check_out).to eq(reservation_params[:check_out]) + end + + it 'adds num_guests to request' do + xml = builder.build_booking_payload(reservation_params) + hash = to_hash(xml) + + num_guests = hash.get("#{stay_info}.NumberOfGuests") + expect(num_guests).to eq(reservation_params[:num_guests].to_s) + end + + it 'adds total price to request' do + xml = builder.build_booking_payload(reservation_params) + hash = to_hash(xml) + + ru_price = hash.get("#{stay_info}.Costs.RUPrice") + client_price = hash.get("#{stay_info}.Costs.ClientPrice") + already_paid = hash.get("#{stay_info}.Costs.AlreadyPaid") + expect(ru_price).to eq(reservation_params[:total]) + expect(client_price).to eq(reservation_params[:total]) + expect(already_paid).to eq(reservation_params[:total]) + end + + it 'adds user information to request' do + xml = builder.build_booking_payload(reservation_params) + hash = to_hash(xml) + + first_name = hash.get("#{reservation_root}.CustomerInfo.Name") + last_name = hash.get("#{reservation_root}.CustomerInfo.SurName") + email = hash.get("#{reservation_root}.CustomerInfo.Email") + phone = hash.get("#{reservation_root}.CustomerInfo.Phone") + address = hash.get("#{reservation_root}.CustomerInfo.Address") + postal_code = hash.get("#{reservation_root}.CustomerInfo.ZipCode") + + expect(first_name).to eq(reservation_params[:user][:first_name]) + expect(last_name).to eq(reservation_params[:user][:last_name]) + expect(email).to eq(reservation_params[:user][:email]) + expect(phone).to eq(reservation_params[:user][:phone]) + expect(address).to eq(reservation_params[:user][:address]) + expect(postal_code).to eq(reservation_params[:user][:postal_code]) + end + end + + describe '#build_cancel_payload' do + let(:params) do + { + reference_number: "999123" + } + end + + it 'embedds username and password to request' do + xml = builder.build_cancel_payload(params[:reference_number]) + hash = to_hash(xml) + + authentication = hash.get("Push_CancelReservation_RQ.Authentication") + expect(authentication.get("UserName")).to eq(credentials.username) + expect(authentication.get("Password")).to eq(credentials.password) + end + + it 'adds reference_number to request' do + xml = builder.build_cancel_payload(params[:reference_number]) + hash = to_hash(xml) + + reservation_id = hash.get("Push_CancelReservation_RQ.ReservationID") + expect(reservation_id).to eq(params[:reference_number]) + end + end + + private + def to_hash(xml) + Concierge::SafeAccessHash.new(Nori.new.parse(xml)) + end +end diff --git a/spec/lib/concierge/suppliers/rentals_united/response_parser_spec.rb b/spec/lib/concierge/suppliers/rentals_united/response_parser_spec.rb new file mode 100644 index 000000000..29362a70b --- /dev/null +++ b/spec/lib/concierge/suppliers/rentals_united/response_parser_spec.rb @@ -0,0 +1,26 @@ +require "spec_helper" + +RSpec.describe RentalsUnited::ResponseParser do + let(:xml_response) do + "Some Response" + end + + let(:bad_xml_response) { 'Server Error' } + + it "converts XML response to a safe hash object with given attributes" do + response_parser = described_class.new + hash = response_parser.to_hash(xml_response) + + expect(hash).to be_kind_of(Concierge::SafeAccessHash) + expect(hash.get("response")).to eq("Some Response") + expect(hash.to_h).to eq({ "response" => "Some Response" }) + end + + it "returns an empty object in case when XML format is not correct" do + response_parser = described_class.new + hash = response_parser.to_hash(bad_xml_response) + + expect(hash).to be_kind_of(Concierge::SafeAccessHash) + expect(hash.to_h).to eq({}) + end +end diff --git a/spec/lib/concierge/suppliers/waytostay/client_spec.rb b/spec/lib/concierge/suppliers/waytostay/client_spec.rb index 98db85e8b..3c8476a40 100644 --- a/spec/lib/concierge/suppliers/waytostay/client_spec.rb +++ b/spec/lib/concierge/suppliers/waytostay/client_spec.rb @@ -146,7 +146,7 @@ end describe "#quote" do - + let(:host) { create_host(fee_percentage: 7.0) } let(:quote_url) { base_url + Waytostay::Quote::ENDPOINT } let(:quote_post_body) {{ @@ -181,6 +181,12 @@ stub_call(:post, quote_url, body: timeout_waytostay_params.to_json, strict: true) { raise Faraday::TimeoutError } + + + %w(success unavailable less_than_min malformed_response, + earlier_than_cutoff timeout).each do |id| + create_property(identifier: id, host_id: host.id) + end end it_behaves_like "supplier quote method" do diff --git a/spec/workers/suppliers/rentals_united/metadata_spec.rb b/spec/workers/suppliers/rentals_united/metadata_spec.rb new file mode 100644 index 000000000..56df988bc --- /dev/null +++ b/spec/workers/suppliers/rentals_united/metadata_spec.rb @@ -0,0 +1,421 @@ +require "spec_helper" + +RSpec.describe Workers::Suppliers::RentalsUnited::Metadata do + include Support::Factories + include Support::HTTPStubbing + include Support::Fixtures + + let(:host) { create_host } + let(:supplier_name) { RentalsUnited::Client::SUPPLIER_NAME } + let(:credentials) { Concierge::Credentials.for(supplier_name) } + let(:url) { credentials.url } + + describe "#perform operation" do + let(:worker) do + described_class.new(host) + end + + it "fails when fetching owner returns an error" do + failing_owner_fetch! + + result = worker.perform + expect(result).to be_nil + expect(worker.property_sync.sync_record.successful).to be false + + expect_sync_error("Failed to fetch owner with owner_id `host`") + end + + it "fails when fetching properties collection for owner returns an error" do + successful_owner_fetch! + failing_properties_collection_fetch! + + result = worker.perform + expect(result).to be_nil + expect(worker.property_sync.sync_record.successful).to be false + + expect_sync_error("Failed to fetch property ids collection for owner `host`") + end + + it "fails when fetching locations by location_ids returns an error" do + successful_owner_fetch! + successful_properties_collection_fetch! + failing_locations_fetch! + + result = worker.perform + expect(result).to be_nil + expect(worker.property_sync.sync_record.successful).to be false + + expect_sync_error("Failed to fetch locations with ids `[\"1505\"]`") + end + + it "fails when fetching location currencies returns an error" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + failing_location_currencies_fetch! + + result = worker.perform + expect(result).to be_nil + expect(worker.property_sync.sync_record.successful).to be false + + expect_sync_error("Failed to fetch locations-currencies mapping") + end + + it "finishes sync when there is no properties to iterate on" do + successful_owner_fetch! + successful_but_empty_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(worker.property_sync.sync_record.successful).to be true + end + + it "fails when there is no location for property and continues worker process" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_but_wrong_locations_fetch! + successful_location_currencies_fetch! + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(worker.property_sync.sync_record.successful).to be true + + expect_sync_error("Failed to find location with id `1505`") + end + + it "fails when there is no currency for location and continues worker process" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_but_wrong_location_currencies_fetch! + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(worker.property_sync.sync_record.successful).to be true + + expect_sync_error("Failed to find currency for location with id `1505`") + end + + it "fails when #fetch_property returns an error" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + failing_property_fetch! + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(worker.property_sync.sync_record.successful).to be false + + expect_sync_error("Failed to fetch property with property_id `519688`") + end + + it "fails when #fetch_seasons returns an error" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + failing_seasons_fetch! + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(worker.property_sync.sync_record.successful).to be false + + expect_sync_error("Failed to fetch seasons for property `519688`") + end + + described_class::IGNORABLE_ERROR_CODES.each do |code| + it "skips property from publishing when there was #{code} error" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + successful_seasons_fetch! + failing_property_build!(code) + + expected_property_ids = ["519688"] + expected_property_ids.each do |property_id| + expect { + sync_process = worker.perform + + expect(sync_process.stats.get("properties_skipped")).to eq( + [{ "reason" => code, "ids" => [property_id] }] + ) + expect(worker.property_sync).not_to( + receive(:start).with(property_id) + ) + }.to change { PropertyRepository.count }.by(0) + end + expect(worker.property_sync.sync_record.successful).to be true + end + end + + it "calls synchronisation block for every property id" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + successful_seasons_fetch! + + expected_property_ids = ["519688"] + expected_property_ids.each do |property_id| + expect(worker.property_sync).to receive(:start).with(property_id) + end + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(result.to_h[:successful]).to be true + expect(worker.property_sync.sync_record.successful).to be true + end + + it "creates record in the database" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + successful_seasons_fetch! + successful_publishing_to_roomorama! + + expect { + worker.perform + }.to change { PropertyRepository.count }.by(1) + expect(worker.property_sync.sync_record.successful).to be true + end + + it "doesnt create property with unsuccessful publishing" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + successful_seasons_fetch! + failing_publishing_to_roomorama! + + expect { + worker.perform + }.to_not change { PropertyRepository.count } + expect(worker.property_sync.sync_record.successful).to be true + end + + it "starts calendar sync when property" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + successful_seasons_fetch! + successful_publishing_to_roomorama! + already_synced_property! + + expected_property_ids = ["519688"] + expected_property_ids.each do |property_id| + expect(worker.calendar_sync).to receive(:start).with(property_id) + end + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(result.to_h[:successful]).to be true + expect(worker.calendar_sync.sync_record.successful).to be true + end + + it "fails when #fetch_availabilities returns an error" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + successful_seasons_fetch! + successful_publishing_to_roomorama! + already_synced_property! + failing_availabilities_fetch! + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + + expect_sync_error("Failed to fetch availabilities for property `519688`") + end + + it "finishes everything" do + successful_owner_fetch! + successful_properties_collection_fetch! + successful_locations_fetch! + successful_location_currencies_fetch! + successful_property_fetch! + successful_seasons_fetch! + successful_publishing_to_roomorama! + already_synced_property! + successful_availabilities_fetch! + + result = worker.perform + expect(result).to be_kind_of(SyncProcess) + expect(result.to_h[:successful]).to be true + end + + private + def expect_sync_error(message) + context_event = Concierge.context.events.last.to_h + + expect(context_event[:label]).to eq("Synchronisation Failure") + expect(context_event[:message]).to eq(message) + expect(context_event[:backtrace]).to be_kind_of(Array) + expect(context_event[:backtrace].any?).to be true + end + + def failing_owner_fetch! + stub_importer_action!(:fetch_owner, Result.error('fail')) + end + + def failing_properties_collection_fetch! + stub_importer_action!( + :fetch_properties_collection_for_owner, + Result.error('fail') + ) + end + + def failing_locations_fetch! + stub_importer_action!(:fetch_locations, Result.error('fail')) + end + + def failing_location_currencies_fetch! + stub_importer_action!(:fetch_location_currencies, Result.error('fail')) + end + + def failing_property_fetch! + stub_importer_action!(:fetch_property, Result.error('fail')) + end + + def failing_seasons_fetch! + stub_importer_action!(:fetch_seasons, Result.error('fail')) + end + + def failing_availabilities_fetch! + stub_importer_action!(:fetch_availabilities, Result.error('fail')) + end + + def failing_publishing_to_roomorama! + stub_publishing_to_roomorama!(Result.error('fail')) + end + + def failing_property_build!(code) + allow_any_instance_of(RentalsUnited::Mappers::RoomoramaProperty) + .to receive(:build_roomorama_property) { Result.error(code) } + end + + def successful_owner_fetch! + owner = double( + id: 'host', + first_name: 'John', + last_name: 'Doe', + email: 'john.doe@gmail.com', + phone: '3128329138' + ) + + stub_importer_action!(:fetch_owner, Result.new(owner)) + end + + def successful_properties_collection_fetch! + collection = RentalsUnited::Entities::PropertiesCollection.new( + [ + { property_id: '519688', location_id: '1505' } + ] + ) + + stub_importer_action!( + :fetch_properties_collection_for_owner, + Result.new(collection) + ) + end + + def successful_but_empty_properties_collection_fetch! + collection = RentalsUnited::Entities::PropertiesCollection.new([]) + + stub_importer_action!( + :fetch_properties_collection_for_owner, + Result.new(collection) + ) + end + + def successful_locations_fetch! + location = RentalsUnited::Entities::Location.new("1505") + location.country = "France" + + stub_importer_action!(:fetch_locations, Result.new([location])) + end + + def successful_but_wrong_locations_fetch! + location = RentalsUnited::Entities::Location.new("1506") + location.country = "France" + + stub_importer_action!(:fetch_locations, Result.new([location])) + end + + def successful_location_currencies_fetch! + location_currencies = {"1505" => "EUR", "1606" => "USD"} + + stub_importer_action!( + :fetch_location_currencies, + Result.new(location_currencies) + ) + end + + def successful_but_wrong_location_currencies_fetch! + location_currencies = {"2505" => "EUR", "2606" => "USD"} + + stub_importer_action!( + :fetch_location_currencies, + Result.new(location_currencies) + ) + end + + def successful_property_fetch! + stub_data = read_fixture("rentals_united/properties/property.xml") + stub_call(:post, url) { [200, {}, stub_data] } + end + + def successful_seasons_fetch! + season = RentalsUnited::Entities::Season.new( + date_from: Date.parse("2016-09-01"), + date_to: Date.parse("2016-09-30"), + price: 200.00 + ) + stub_importer_action!(:fetch_seasons, Result.new([season])) + end + + def successful_availabilities_fetch! + availability = RentalsUnited::Entities::Availability.new( + date: Date.parse("2016-09-01"), + available: true, + minimum_stay: 1, + changeover: "4" + ) + stub_importer_action!(:fetch_availabilities, Result.new([availability])) + end + + def successful_publishing_to_roomorama! + stub_publishing_to_roomorama!(Result.new('success')) + end + + def already_synced_property! + allow_any_instance_of(Hanami::Model::Adapters::Sql::Query) + .to receive(:count) { 1 } + end + + def stub_publishing_to_roomorama!(result) + allow_any_instance_of(Roomorama::Client).to receive(:perform) do + result + end + end + + def stub_importer_action!(action, result) + expect_any_instance_of(RentalsUnited::Importer) + .to(receive(action)) + .and_return(result) + end + end +end