hotel_beds 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/INTERNALS.md +6 -0
  3. data/README.md +55 -29
  4. data/lib/hotel_beds/client.rb +11 -3
  5. data/lib/hotel_beds/connection.rb +2 -3
  6. data/lib/hotel_beds/hotel_basket_add/envelope.rb +71 -0
  7. data/lib/hotel_beds/hotel_basket_add/operation.rb +36 -0
  8. data/lib/hotel_beds/hotel_basket_add/request.rb +49 -0
  9. data/lib/hotel_beds/hotel_basket_add/response.rb +66 -0
  10. data/lib/hotel_beds/hotel_search/envelope.rb +27 -18
  11. data/lib/hotel_beds/hotel_search/parser/errors.rb +49 -0
  12. data/lib/hotel_beds/hotel_search/parser/hotel.rb +67 -0
  13. data/lib/hotel_beds/hotel_search/parser/price.rb +38 -0
  14. data/lib/hotel_beds/hotel_search/parser/room.rb +46 -0
  15. data/lib/hotel_beds/hotel_search/parser/room_grouper.rb +101 -0
  16. data/lib/hotel_beds/hotel_search/request.rb +5 -23
  17. data/lib/hotel_beds/hotel_search/response.rb +16 -68
  18. data/lib/hotel_beds/model.rb +0 -1
  19. data/lib/hotel_beds/model/available_room.rb +9 -11
  20. data/lib/hotel_beds/model/cancellation_policy.rb +14 -0
  21. data/lib/hotel_beds/model/hotel.rb +4 -0
  22. data/lib/hotel_beds/model/purchase.rb +16 -0
  23. data/lib/hotel_beds/model/requested_room.rb +7 -0
  24. data/lib/hotel_beds/model/room.rb +31 -0
  25. data/lib/hotel_beds/model/search_result.rb +0 -5
  26. data/lib/hotel_beds/model/service.rb +18 -0
  27. data/lib/hotel_beds/version.rb +1 -1
  28. data/spec/features/adding_a_hotel_to_basket_spec.rb +58 -0
  29. data/spec/features/hotel_search_spec.rb +31 -9
  30. data/spec/lib/hotel_beds/hotel_search/parser/room_grouper_spec.rb +53 -0
  31. data/spec/spec_helper.rb +8 -0
  32. metadata +20 -5
  33. data/lib/hotel_beds/hotel_search/room_grouper.rb +0 -64
  34. data/spec/lib/hotel_beds/hotel_search/room_grouper_spec.rb +0 -45
@@ -0,0 +1,49 @@
1
+ require "active_model/errors"
2
+
3
+ module HotelBeds
4
+ module HotelSearch
5
+ module Parser
6
+ class Errors
7
+ def self.call(*args, &block)
8
+ new(*args, &block).call
9
+ end
10
+
11
+ attr_accessor :response, :body
12
+ private :response, :body
13
+
14
+ def initialize(response, body)
15
+ self.response = response
16
+ self.body = body
17
+ freeze
18
+ end
19
+
20
+ def call
21
+ ActiveModel::Errors.new(self).tap do |errors|
22
+ apply_response_errors(errors)
23
+ apply_body_errors(errors)
24
+ end.freeze
25
+ end
26
+
27
+ private
28
+ def apply_response_errors(errors)
29
+ if response.http_error?
30
+ errors.add(:base, "HTTP error")
31
+ elsif response.soap_fault?
32
+ errors.add(:base, "SOAP error")
33
+ elsif !response.success?
34
+ errors.add(:base, "Request failed")
35
+ end
36
+ end
37
+
38
+ def apply_body_errors(errors)
39
+ body.css("ErrorList Error").each do |error|
40
+ errors.add(:base, [
41
+ (sm = error.at_css("Message")) && sm.content,
42
+ (dm = error.at_css("DetailedMessage")) && dm.content
43
+ ].compact.join("\n"))
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,67 @@
1
+ require "ostruct"
2
+ require "hotel_beds/model/hotel"
3
+ require "hotel_beds/hotel_search/parser/room"
4
+ require "hotel_beds/hotel_search/parser/room_grouper"
5
+
6
+ module HotelBeds
7
+ module HotelSearch
8
+ module Parser
9
+ class Hotel
10
+ def self.call(*args, &block)
11
+ new(*args, &block).call
12
+ end
13
+
14
+ attr_accessor :hotel, :currency, :request
15
+ private :hotel=, :currency=, :request=
16
+
17
+ def initialize(hotel:, currency:, request:)
18
+ self.hotel = hotel
19
+ self.currency = currency
20
+ self.request = request
21
+ freeze
22
+ end
23
+
24
+ def call
25
+ HotelBeds::Model::Hotel.new({
26
+ availability_token: hotel.attr("availToken"),
27
+ id: hotel.at_css("HotelInfo Code").content,
28
+ name: hotel.at_css("HotelInfo Name").content,
29
+ images: hotel.css("HotelInfo ImageList Image Url").map(&:content),
30
+ latitude: hotel.at_css("HotelInfo Position").attr("latitude"),
31
+ longitude: hotel.at_css("HotelInfo Position").attr("longitude"),
32
+ results: results,
33
+ destination_code: hotel.at_css("HotelInfo Destination").attr("code"),
34
+ contract_name: hotel.at_css("ContractList Contract Name").content,
35
+ contract_incoming_office_code: hotel.at_css("ContractList Contract IncomingOffice").attr("code")
36
+ })
37
+ end
38
+
39
+ private
40
+ def rooms
41
+ hotel.css("AvailableRoom")
42
+ end
43
+
44
+ def results
45
+ group_rooms(parsed_available_rooms).map do |rooms|
46
+ { rooms: Array(rooms), currency: currency }
47
+ end
48
+ end
49
+
50
+ def group_rooms(rooms)
51
+ if request.group_results?
52
+ grouper = HotelBeds::HotelSearch::Parser::RoomGrouper
53
+ grouper.new(request.rooms, rooms).results
54
+ else
55
+ rooms
56
+ end
57
+ end
58
+
59
+ def parsed_available_rooms
60
+ Array(rooms).map do |room|
61
+ HotelBeds::HotelSearch::Parser::Room.call(room)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,38 @@
1
+ module HotelBeds
2
+ module HotelSearch
3
+ module Parser
4
+ class Price
5
+ def self.call(*args, &block)
6
+ new(*args, &block).call
7
+ end
8
+
9
+ attr_accessor :price
10
+ private :price
11
+
12
+ def initialize(price)
13
+ self.price = price
14
+ freeze
15
+ end
16
+
17
+ def call
18
+ (from..to).to_a.inject(Hash.new) do |result, date|
19
+ result.merge!(date => amount)
20
+ end
21
+ end
22
+
23
+ private
24
+ def from
25
+ Date.parse(price.at_css("DateTimeFrom").attr("date"))
26
+ end
27
+
28
+ def to
29
+ Date.parse(price.at_css("DateTimeTo").attr("date"))
30
+ end
31
+
32
+ def amount
33
+ BigDecimal.new(price.at_css("Amount").content, 3)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ require "hotel_beds/hotel_search/parser/price"
2
+ require "hotel_beds/model/available_room"
3
+
4
+ module HotelBeds
5
+ module HotelSearch
6
+ module Parser
7
+ class Room
8
+ def self.call(*args, &block)
9
+ new(*args, &block).call
10
+ end
11
+
12
+ attr_accessor :room
13
+ private :room=
14
+
15
+ def initialize(room)
16
+ self.room = room
17
+ freeze
18
+ end
19
+
20
+ def call
21
+ HotelBeds::Model::AvailableRoom.new({
22
+ id: room.at_css("HotelRoom").attr("SHRUI"),
23
+ room_count: room.at_css("HotelOccupancy RoomCount").content,
24
+ adult_count: room.at_css("HotelOccupancy AdultCount").content,
25
+ child_count: room.at_css("HotelOccupancy ChildCount").content,
26
+ number_available: room.at_css("HotelRoom").attr("availCount"),
27
+ description: room.at_css("HotelRoom RoomType").content,
28
+ board: room.at_css("HotelRoom Board").content,
29
+ board_code: room.at_css("HotelRoom Board").attr("code"),
30
+ room_type_code: room.at_css("HotelRoom RoomType").attr("code"),
31
+ room_type_characteristic: room.at_css("HotelRoom RoomType").attr("characteristic"),
32
+ price: ((room.at_css("HotelRoom") > "Price") > "Amount").first.content,
33
+ rates: parse_price_list(room.css("Price PriceList Price"))
34
+ })
35
+ end
36
+
37
+ private
38
+ def parse_price_list(prices)
39
+ Array(prices).map do |price|
40
+ HotelBeds::HotelSearch::Parser::Price.call(price)
41
+ end.inject(Hash.new, :merge)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,101 @@
1
+ # see INTERNALS.md for comment symbols
2
+ module HotelBeds
3
+ module HotelSearch
4
+ module Parser
5
+ class RoomGrouper < Struct.new(:requested_rooms, :response_rooms)
6
+ def results
7
+ unique_room_combinations
8
+ end
9
+
10
+ private
11
+ def unique_room_combinations
12
+ combinations = build_combinations(room_options_grouped_by_occupants)
13
+ unique_combinations(expand_combinations(combinations))
14
+ end
15
+
16
+ def unique_combinations(combinations)
17
+ combinations.uniq { |r| r.sort_by(&:id).map(&:id) }
18
+ end
19
+
20
+ def expand_combinations(combinations)
21
+ combinations.map do |rooms|
22
+ rooms.inject(Array.new) do |result, room|
23
+ # get an array of all the child ages
24
+ child_ages = requested_rooms_child_ages_by_occupants.fetch(occupant_key(room))
25
+ 1.upto(room.room_count) do |i|
26
+ result << room.dup.tap do |r|
27
+ r.room_count = 1
28
+ r.child_ages = child_ages.pop
29
+ end
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
35
+
36
+ # returns an array of room combinations for all rooms
37
+ def build_combinations(combinations)
38
+ head, *rest = combinations
39
+ Array(head).product(*rest)
40
+ end
41
+
42
+ # returns a array of arrays, each contains available rooms for a given
43
+ # room occupancy
44
+ def room_options_grouped_by_occupants
45
+ requested_room_count_by_occupants.inject(Array.new) do |result, (key, count)|
46
+ result << response_rooms_by_occupants.fetch(key).select do |room|
47
+ room.room_count == count
48
+ end
49
+ end
50
+ rescue KeyError => e
51
+ Array.new
52
+ end
53
+
54
+ # returns a hash of OK => [Integer]
55
+ def requested_rooms_child_ages_by_occupants
56
+ requested_rooms.inject(Hash.new) do |result, room|
57
+ key = occupant_key(room)
58
+ result[key] ||= Array.new
59
+ result[key] += room.child_ages
60
+ result
61
+ end
62
+ end
63
+
64
+ # returns a hash of OK => [RR]
65
+ def requested_rooms_by_occupants
66
+ requested_rooms.inject(Hash.new) do |result, room|
67
+ key = occupant_key(room)
68
+ result[key] ||= Array.new
69
+ result[key] << room
70
+ result
71
+ end
72
+ end
73
+
74
+ # returns a hash of OK => 1 (count)
75
+ def requested_room_count_by_occupants
76
+ requested_rooms_by_occupants.inject(Hash.new) do |result, (key, rooms)|
77
+ result.merge(key => rooms.size)
78
+ end
79
+ end
80
+
81
+ # returns a hash of OK => [AvailableRoom, AvailableRoom, AvailableRoom]
82
+ def response_rooms_by_occupants
83
+ response_rooms.inject(Hash.new) do |result, room|
84
+ key = occupant_key(room)
85
+ result[key] ||= Array.new
86
+ result[key].push(room)
87
+ result
88
+ end
89
+ end
90
+
91
+ # returns an OK for a given room
92
+ def occupant_key(room)
93
+ {
94
+ adult_count: room.adult_count,
95
+ child_count: room.child_count
96
+ }
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -1,44 +1,26 @@
1
+ require "securerandom"
1
2
  require "hotel_beds/model"
3
+ require "hotel_beds/model/requested_room"
2
4
 
3
5
  module HotelBeds
4
6
  module HotelSearch
5
7
  class Request
6
- class Room
7
- include HotelBeds::Model
8
-
9
- # attributes
10
- attribute :adult_count, Integer, default: 0
11
- attribute :child_count, Integer, default: 0
12
- attribute :child_ages, Array[Integer], default: Array.new
13
-
14
- # validation
15
- validates :adult_count, :child_count, numericality: {
16
- greater_than_or_equal_to: 0,
17
- less_than_or_equal_to: 4,
18
- only_integer: true,
19
- }
20
- validate do |room|
21
- unless child_ages.compact.size == child_count
22
- room.errors.add(:child_ages, "must match quantity of children")
23
- end
24
- end
25
- end
26
-
27
8
  include HotelBeds::Model
28
9
 
29
10
  # attributes
11
+ attribute :session_id, String, default: SecureRandom.hex[0..15]
30
12
  attribute :page_number, Integer, default: 1
31
13
  attribute :language, String, default: "ENG"
32
14
  attribute :check_in_date, Date
33
15
  attribute :check_out_date, Date
34
16
  attribute :destination, String
35
17
  attribute :hotels, Array[Integer]
36
- attribute :rooms, Array[Room]
18
+ attribute :rooms, Array[HotelBeds::Model::RequestedRoom]
37
19
  attribute :group_results, Virtus::Attribute::Boolean, default: false
38
20
 
39
21
  # validation
40
22
  validates :language, :destination, length: { is: 3 }
41
- validates :check_in_date, :check_out_date, presence: true
23
+ validates :session_id, :check_in_date, :check_out_date, presence: true
42
24
  validates :rooms, length: { minimum: 1, maximum: 5 }
43
25
  validates :page_number, numericality: {
44
26
  greater_than: 0, only_integer: true
@@ -1,7 +1,6 @@
1
- require "active_model/errors"
2
1
  require "hotel_beds/model/hotel"
3
- require "hotel_beds/model/available_room"
4
- require_relative "room_grouper"
2
+ require "hotel_beds/hotel_search/parser/errors"
3
+ require "hotel_beds/hotel_search/parser/hotel"
5
4
 
6
5
  module HotelBeds
7
6
  module HotelSearch
@@ -13,22 +12,7 @@ module HotelBeds
13
12
  self.request = request
14
13
  self.headers = response.header
15
14
  self.body = Nokogiri::XML(response.body.fetch(:get_hotel_valued_avail))
16
- self.errors = ActiveModel::Errors.new(self).tap do |errors|
17
- if response.http_error?
18
- errors.add(:base, "HTTP error")
19
- elsif response.soap_fault?
20
- errors.add(:base, "SOAP error")
21
- elsif !response.success?
22
- errors.add(:base, "Request failed")
23
- end
24
-
25
- body.css("ErrorList Error").each do |error|
26
- errors.add(:base, [
27
- (sm = error.at_css("Message")) && sm.content,
28
- (dm = error.at_css("DetailedMessage")) && dm.content
29
- ].compact.join("\n"))
30
- end
31
- end
15
+ self.errors = HotelBeds::HotelSearch::Parser::Errors.call(response, body)
32
16
  freeze
33
17
  end
34
18
 
@@ -36,6 +20,10 @@ module HotelBeds
36
20
  "<#{self.class.name} errors=#{errors.inspect} headers=#{headers.inspect} body=#{body.to_s}>"
37
21
  end
38
22
 
23
+ def session_id
24
+ request.session_id
25
+ end
26
+
39
27
  def current_page
40
28
  if data = pagination_data
41
29
  Integer(data.attr("currentPage"))
@@ -52,16 +40,17 @@ module HotelBeds
52
40
  end
53
41
  end
54
42
 
43
+ def currency
44
+ body.at_css("Currency").attr("code")
45
+ end
46
+
55
47
  def hotels
56
48
  body.css("ServiceHotel").lazy.map do |hotel|
57
- HotelBeds::Model::Hotel.new({
58
- id: hotel.at_css("HotelInfo Code").content,
59
- name: hotel.at_css("HotelInfo Name").content,
60
- images: hotel.css("HotelInfo ImageList Image Url").map(&:content),
61
- latitude: hotel.at_css("HotelInfo Position").attr("latitude"),
62
- longitude: hotel.at_css("HotelInfo Position").attr("longitude"),
63
- results: generate_results(hotel.css("AvailableRoom"))
64
- })
49
+ HotelBeds::HotelSearch::Parser::Hotel.call(
50
+ hotel: hotel,
51
+ currency: currency,
52
+ request: request
53
+ )
65
54
  end
66
55
  end
67
56
 
@@ -69,47 +58,6 @@ module HotelBeds
69
58
  def pagination_data
70
59
  body.at_css("PaginationData")
71
60
  end
72
-
73
- def generate_results(rooms)
74
- parsed_rooms = parse_available_rooms(rooms)
75
- if request.group_results?
76
- parsed_rooms = RoomGrouper.new(request.rooms, parsed_rooms).results
77
- end
78
- parsed_rooms.map do |rooms|
79
- HotelBeds::Model::SearchResult.new({
80
- rooms: Array(rooms),
81
- currency: body.at_css("Currency").attr("code")
82
- })
83
- end
84
- end
85
-
86
- def parse_available_rooms(rooms)
87
- Array(rooms).map do |room|
88
- HotelBeds::Model::AvailableRoom.new({
89
- id: room.at_css("HotelRoom").attr("SHRUI"),
90
- adult_count: room.at_css("HotelOccupancy AdultCount").content,
91
- child_count: room.at_css("HotelOccupancy ChildCount").content,
92
- number_available: room.at_css("HotelRoom").attr("availCount"),
93
- description: room.at_css("HotelRoom RoomType").content,
94
- board: room.at_css("HotelRoom Board").content,
95
- price: ((room.at_css("HotelRoom") > "Price") > "Amount").first.content,
96
- rates: parse_price_list(room.css("Price PriceList Price"))
97
- })
98
- end
99
- end
100
-
101
- def parse_price_list(prices)
102
- Array(prices).inject({}) do |result, price|
103
- from = Date.parse(price.at_css("DateTimeFrom").attr("date"))
104
- to = Date.parse(price.at_css("DateTimeTo").attr("date"))
105
- dates = (from..to).to_a
106
- amount = BigDecimal.new(price.at_css("Amount").content, 3)
107
- dates.each do |date|
108
- result.merge!(date => amount)
109
- end
110
- result
111
- end
112
- end
113
61
  end
114
62
  end
115
63
  end