hotel_beds 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 62d1e73612f9067924e5217e9a6ac519114c3c0f
4
- data.tar.gz: 3d73a53b48fc47773dc9d11b54f1d2d97862586c
3
+ metadata.gz: b442fa8e1dd84060190628131cdc1411cd170780
4
+ data.tar.gz: 3f0e1420814abb2208119060056f66f813ef32e2
5
5
  SHA512:
6
- metadata.gz: fcd299beabb15ae42f27b1398fb88db2d4901ab80a06987b35182d0fc494421377b7d8abf3d6d496627c0af469e98994c32c91ba79c5a8e89ad1d239caa311c9
7
- data.tar.gz: 6613c409737fc9b5b75d946a7681e1ee5555bef3a9422d2e5e128d9c77945db776e89c93abbb72d61bbd2ce9d7de4e6c2ecaeb947b7785eec97dea84959b6a6c
6
+ metadata.gz: 873348c0c1b44e5218efd5f0f784cf6c235ea9e61327f312b40e474925e99a04d7406fcf882ecafa447d4bed2742fc4687a4247293824898c763608eb2f9bd48
7
+ data.tar.gz: 9405c30f24d1b2fd0e4428f70ed96205059ed97dd4b0f3143fa6d4036d7dd8f960c5e6a62da847621c17d5af6d251b138983bbbcc2adda9dfaef2bb69a7e71b2
data/INTERNALS.md CHANGED
@@ -7,3 +7,9 @@ The process we use to group results is as follows:
7
7
  3) Group the count of each requested room (RC) by occupants (OK)
8
8
  4) For each group, determine all combinations of the available rooms (AR) for the given count (RC), this gives us the search results per room group (RSRG)
9
9
  5) Find all combinations of the search results per room group, giving us unique search results (SR)
10
+
11
+ # Basket
12
+
13
+ HotelBeds effectively has a basket system (known as the "ServiceAdd" API), as it covers more than just hotel rooms.
14
+
15
+ The "contract remarks" or terms and conditions it returns must be presented to the customer before the basket is checked-out.
data/README.md CHANGED
@@ -16,24 +16,48 @@ Manually, via command line:
16
16
 
17
17
  ## Usage
18
18
 
19
- # create the connection to HotelBeds
20
- client = HotelBeds::Client.new(endpoint: :test, username: "user", password: "pass")
21
-
22
- # perform the search
23
- search = client.perform_hotel_search({
24
- check_in_date: Date.today,
25
- check_out_date: Date.today + 1,
26
- rooms: [{ adult_count: 2 }],
27
- destination: "SYD"
28
- })
29
-
30
- # inspect the response
31
- puts search.response.hotels
32
- # => [<HotelBeds::Model::Hotel>, <HotelBeds::Model::Hotel>]
33
- puts search.response.total_pages
34
- # => 10
35
- puts search.response.current_page
36
- # => 1
19
+ ```ruby
20
+ # create the connection to HotelBeds
21
+ client = HotelBeds::Client.new(endpoint: :test, username: "user", password: "pass")
22
+
23
+ # perform the search
24
+ search = client.perform_hotel_search({
25
+ check_in_date: Date.today,
26
+ check_out_date: Date.today + 1,
27
+ rooms: [{ adult_count: 2 }],
28
+ destination: "SYD"
29
+ })
30
+
31
+ # inspect the response
32
+ puts search.response.hotels
33
+ # => [<HotelBeds::Model::Hotel>, <HotelBeds::Model::Hotel>]
34
+ puts search.response.total_pages
35
+ # => 10
36
+ puts search.response.current_page
37
+ # => 1
38
+
39
+ # place a booking
40
+ booking = client.perform_hotel_booking({
41
+ room_ids: [search.response.hotels.first.results.first.id],
42
+ people: [
43
+ { title: "Mr", name: "David Smith", type: :adult },
44
+ { title: "Mrs", name: "Jane Smith", type: :adult }
45
+ ],
46
+ address: {
47
+ line_1: "123 Some Street",
48
+ city: "Townsville",
49
+ state: "New Statestown",
50
+ postcode: "NS1 1AB"
51
+ country: "UK"
52
+ },
53
+ phone_number: "+44 1234 567 890",
54
+ email: "david.smith@example.com"
55
+ })
56
+
57
+ # inspect the response
58
+ puts booking.response.reference
59
+ # => "ABC-123"
60
+ ```
37
61
 
38
62
  ### Options
39
63
 
@@ -41,17 +65,19 @@ The HotelBeds API will return individual rooms, rather than being grouped by wha
41
65
 
42
66
  Example:
43
67
 
44
- # perform the search
45
- search = client.perform_hotel_search({
46
- check_in_date: Date.today,
47
- check_out_date: Date.today + 1,
48
- rooms: [
49
- { adult_count: 2 },
50
- { adult_count: 1, child_count: 1, child_ages: [7] }
51
- ],
52
- destination: "SYD",
53
- group_results: true
54
- })
68
+ ```ruby
69
+ # perform the search
70
+ search = client.perform_hotel_search({
71
+ check_in_date: Date.today,
72
+ check_out_date: Date.today + 1,
73
+ rooms: [
74
+ { adult_count: 2 },
75
+ { adult_count: 1, child_count: 1, child_ages: [7] }
76
+ ],
77
+ destination: "SYD",
78
+ group_results: true
79
+ })
80
+ ```
55
81
 
56
82
  ## Contributing
57
83
 
@@ -1,6 +1,7 @@
1
1
  require "hotel_beds/configuration"
2
2
  require "hotel_beds/connection"
3
3
  require "hotel_beds/hotel_search/operation"
4
+ require "hotel_beds/hotel_basket_add/operation"
4
5
 
5
6
  module HotelBeds
6
7
  class Client
@@ -12,13 +13,20 @@ module HotelBeds
12
13
  self.connection = Connection.new(configuration)
13
14
  freeze
14
15
  end
15
-
16
- # builds and performs a hotel search, returning an operation object which
17
- # contains both the request and response objects.
16
+
17
+ # each method returns an operation object which contains both the
18
+ # request and response objects.
19
+
18
20
  def perform_hotel_search(*args)
19
21
  HotelSearch::Operation.new(*args).perform(
20
22
  connection: connection
21
23
  )
22
24
  end
25
+
26
+ def add_hotel_room_to_basket(*args)
27
+ HotelBasketAdd::Operation.new(*args).perform(
28
+ connection: connection
29
+ )
30
+ end
23
31
  end
24
32
  end
@@ -18,7 +18,6 @@ module HotelBeds
18
18
  :"@xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
19
19
  :"@xsi:schemaLocation" => "http://www.hotelbeds.com/schemas/2005/06/messages #{namespace}.xsd",
20
20
  :@echoToken => SecureRandom.hex[0..15],
21
- :@sessionId => SecureRandom.hex[0..15],
22
21
  :Credentials => {
23
22
  User: configuration.username,
24
23
  Password: configuration.password
@@ -34,7 +33,7 @@ module HotelBeds
34
33
  message: message
35
34
  })
36
35
  end
37
-
36
+
38
37
  private
39
38
  def initialize_client
40
39
  Savon::Client.new do |config|
@@ -70,4 +69,4 @@ module HotelBeds
70
69
  end
71
70
  end
72
71
  end
73
- end
72
+ end
@@ -0,0 +1,71 @@
1
+ require "delegate"
2
+
3
+ module HotelBeds
4
+ module HotelBasketAdd
5
+ class Envelope < SimpleDelegator
6
+ def attributes
7
+ {
8
+ Language: language,
9
+ Service: {
10
+ :"@xsi:type" => "ServiceHotel",
11
+ :"@availToken" => service.availability_token,
12
+ :ContractList => { Contract: {
13
+ Name: service.contract_name,
14
+ IncomingOffice: { :@code => service.contract_incoming_office_code }
15
+ } },
16
+ :DateFrom => { :@date => service.check_in_date.strftime("%Y%m%d") },
17
+ :DateTo => { :@date => service.check_out_date.strftime("%Y%m%d") },
18
+ :HotelInfo => {
19
+ Code: service.hotel_code,
20
+ Destination: {
21
+ :@code => service.destination_code,
22
+ :@type => "SIMPLE"
23
+ }
24
+ },
25
+ :AvailableRoom => available_rooms
26
+ }
27
+ }
28
+ end
29
+
30
+ private
31
+ def available_rooms
32
+ Array(service.rooms).group_by(&:group_key).values.map do |rooms|
33
+ {
34
+ HotelOccupancy: build_room(rooms),
35
+ HotelRoom: {
36
+ Board: {
37
+ :@code => rooms.first.board_code,
38
+ :@type => "SIMPLE"
39
+ },
40
+ RoomType: {
41
+ :@code => rooms.first.room_type_code,
42
+ :@characteristic => rooms.first.room_type_characteristic,
43
+ :@type => "SIMPLE"
44
+ },
45
+ }
46
+ }
47
+ end
48
+ end
49
+
50
+ def build_room(rooms)
51
+ child_ages = rooms.map(&:child_ages).inject(Array.new, :+)
52
+ adult_count = rooms.map(&:adult_count).inject(0, :+)
53
+ child_count = rooms.map(&:child_count).inject(0, :+)
54
+ {
55
+ RoomCount: rooms.size,
56
+ Occupancy: {
57
+ AdultCount: adult_count,
58
+ ChildCount: child_count,
59
+ GuestList: {
60
+ Customer: (1..adult_count).map {
61
+ { :@type => "AD" }
62
+ } + (1..child_count).map { |i|
63
+ { :@type => "CH", :Age => Integer(child_ages[i - 1]) }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "request"
2
+ require_relative "response"
3
+ require_relative "envelope"
4
+
5
+ module HotelBeds
6
+ module HotelBasketAdd
7
+ class Operation
8
+ attr_accessor :request, :response, :errors
9
+ private :request=, :response=, :errors=
10
+
11
+ def initialize(*args)
12
+ self.request = Request.new(*args)
13
+ end
14
+
15
+ def perform(connection:)
16
+ if request.valid?
17
+ self.response = Response.new(request, retrieve(connection))
18
+ self.errors = response.errors
19
+ else
20
+ self.errors = request.errors
21
+ end
22
+ freeze
23
+ self
24
+ end
25
+
26
+ private
27
+ def retrieve(connection)
28
+ connection.call({
29
+ method: :serviceAdd,
30
+ namespace: :ServiceAddRQ,
31
+ data: Envelope.new(request).attributes
32
+ })
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,49 @@
1
+ require "securerandom"
2
+ require "hotel_beds/model"
3
+ require "hotel_beds/model/available_room"
4
+
5
+ module HotelBeds
6
+ module HotelBasketAdd
7
+ class Request
8
+ class HotelService
9
+ include HotelBeds::Model
10
+
11
+ # attributes
12
+ attribute :availability_token, String
13
+ attribute :contract_name, String
14
+ attribute :contract_incoming_office_code, String
15
+ attribute :check_in_date, Date
16
+ attribute :check_out_date, Date
17
+ attribute :hotel_code, String
18
+ attribute :destination_code, String
19
+ attribute :rooms, Array[HotelBeds::Model::AvailableRoom]
20
+
21
+ # validation
22
+ validates :destination_code, length: { is: 3 }
23
+ validates :rooms, length: { minimum: 1, maximum: 5 }
24
+ validates :session_id, :availability_token, :contract_name,
25
+ :contract_incoming_office_code, :check_in_date, :check_out_date,
26
+ :hotel_code, presence: true
27
+ validate do |service|
28
+ unless (1..5).cover?(service.rooms.size)
29
+ service.errors.add(:rooms, "quantity must be between 1 and 5")
30
+ end
31
+ unless service.rooms.all?(&:valid?)
32
+ service.errors.add(:rooms, "are invalid")
33
+ end
34
+ end
35
+ end
36
+
37
+ include HotelBeds::Model
38
+
39
+ # attributes
40
+ attribute :session_id, String, default: SecureRandom.hex[0..15]
41
+ attribute :language, String, default: "ENG"
42
+ attribute :service, HotelService
43
+
44
+ # validation
45
+ validates :language, length: { is: 3 }
46
+ validates :session_id, :service, presence: true
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,66 @@
1
+ require "active_model/errors"
2
+ require "hotel_beds/model/purchase"
3
+
4
+ module HotelBeds
5
+ module HotelBasketAdd
6
+ class Response
7
+ attr_accessor :request, :headers, :body, :errors
8
+ private :request=, :headers=, :body=, :errors=
9
+
10
+ def initialize(request, response)
11
+ self.request = request
12
+ self.headers = response.header
13
+ self.body = Nokogiri::XML(response.body.fetch(:service_add))
14
+ self.errors = ActiveModel::Errors.new(self).tap do |errors|
15
+ if response.http_error?
16
+ errors.add(:base, "HTTP error")
17
+ elsif response.soap_fault?
18
+ errors.add(:base, "SOAP error")
19
+ elsif !response.success?
20
+ errors.add(:base, "Request failed")
21
+ end
22
+
23
+ body.css("ErrorList Error").each do |error|
24
+ errors.add(:base, [
25
+ (sm = error.at_css("Message")) && sm.content,
26
+ (dm = error.at_css("DetailedMessage")) && dm.content
27
+ ].compact.join("\n"))
28
+ end
29
+ end
30
+ freeze
31
+ end
32
+
33
+ def inspect
34
+ "<#{self.class.name} errors=#{errors.inspect} headers=#{headers.inspect} body=#{body.to_s}>"
35
+ end
36
+
37
+ def session_id
38
+ request.session_id
39
+ end
40
+
41
+ def success?
42
+ errors.empty?
43
+ end
44
+
45
+ def purchase
46
+ purchase = body.at_css("Purchase")
47
+ HotelBeds::Model::Purchase.new({
48
+ id: purchase.attr("purchaseToken"),
49
+ currency: purchase.at_css("Currency").attr("code"),
50
+ amount: purchase.at_css("TotalPrice").content,
51
+ services: purchase.css("ServiceList Service").map { |service|
52
+ {
53
+ id: service.attr("SPUI"),
54
+ contract_name: service.at_css("Contract Name").content,
55
+ contract_incoming_office_code: service.at_css("Contract IncomingOffice").attr("code"),
56
+ check_in_date: Date.parse(service.at_css("DateFrom").attr("date")),
57
+ check_out_date: Date.parse(service.at_css("DateTo").attr("date")),
58
+ currency: service.at_css("Currency").attr("code"),
59
+ amount: service.at_css("TotalAmount").content
60
+ }
61
+ }
62
+ })
63
+ end
64
+ end
65
+ end
66
+ end
@@ -5,11 +5,12 @@ module HotelBeds
5
5
  class Envelope < SimpleDelegator
6
6
  def attributes
7
7
  {
8
- PaginationData: pagination_data,
9
- Language: language,
10
- CheckInDate: check_in_date,
11
- CheckOutDate: check_out_date,
12
- OccupancyList: occupancy_list
8
+ :@sessionId => session_id,
9
+ :PaginationData => pagination_data,
10
+ :Language => language,
11
+ :CheckInDate => check_in_date,
12
+ :CheckOutDate => check_out_date,
13
+ :OccupancyList => occupancy_list
13
14
  }.merge(Hash(destination)).merge(Hash(hotels)).merge(Hash(extra_params))
14
15
  end
15
16
 
@@ -63,20 +64,28 @@ module HotelBeds
63
64
  end
64
65
 
65
66
  def occupancy_list
66
- { HotelOccupancy: Array(rooms).map { |room|
67
- guest_list = if room.child_count > 0
68
- { GuestList: { Customer: Array(room.child_ages).map { |age|
69
- { :@type => "CH", :Age => Integer(age) }
70
- } } }
71
- end
72
- {
73
- RoomCount: 1,
74
- Occupancy: (guest_list || Hash.new).merge({
75
- AdultCount: Integer(room.adult_count),
76
- ChildCount: Integer(room.child_count)
77
- })
67
+ grouped_rooms = Array(rooms).group_by(&:group_key).values
68
+ { HotelOccupancy: grouped_rooms.map(&method(:build_room)) }
69
+ end
70
+
71
+ def build_room(rooms)
72
+ child_ages = rooms.map(&:child_ages).inject(Array.new, :+)
73
+ adult_count = rooms.map(&:adult_count).inject(0, :+)
74
+ child_count = rooms.map(&:child_count).inject(0, :+)
75
+ {
76
+ RoomCount: rooms.size,
77
+ Occupancy: {
78
+ AdultCount: adult_count,
79
+ ChildCount: child_count,
80
+ GuestList: {
81
+ Customer: (1..adult_count).map {
82
+ { :@type => "AD" }
83
+ } + (1..child_count).map { |i|
84
+ { :@type => "CH", :Age => Integer(child_ages[i - 1]) }
85
+ }
86
+ }
78
87
  }
79
- } }
88
+ }
80
89
  end
81
90
  end
82
91
  end