rthbound-suitcase 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +3 -0
- data/README.md +83 -0
- data/Rakefile +13 -0
- data/examples/hash_adapter.rb +15 -0
- data/examples/hotel_image_db.rb +24 -0
- data/examples/redis_adapter.rb +29 -0
- data/lib/suitcase.rb +16 -0
- data/lib/suitcase/car_rental.rb +57 -0
- data/lib/suitcase/codes.rb +5 -0
- data/lib/suitcase/configuration.rb +29 -0
- data/lib/suitcase/core_ext/string.rb +13 -0
- data/lib/suitcase/hotel.rb +355 -0
- data/lib/suitcase/hotel/amenity.rb +46 -0
- data/lib/suitcase/hotel/bed_type.rb +21 -0
- data/lib/suitcase/hotel/cache.rb +52 -0
- data/lib/suitcase/hotel/ean_exception.rb +35 -0
- data/lib/suitcase/hotel/helpers.rb +198 -0
- data/lib/suitcase/hotel/image.rb +19 -0
- data/lib/suitcase/hotel/location.rb +67 -0
- data/lib/suitcase/hotel/nightly_rate.rb +14 -0
- data/lib/suitcase/hotel/payment_option.rb +41 -0
- data/lib/suitcase/hotel/reservation.rb +15 -0
- data/lib/suitcase/hotel/room.rb +138 -0
- data/lib/suitcase/hotel/session.rb +7 -0
- data/lib/suitcase/hotel/surcharge.rb +23 -0
- data/lib/suitcase/version.rb +3 -0
- data/rthbound-suitcase.gemspec +30 -0
- data/test/car_rentals/car_rental_test.rb +30 -0
- data/test/hotels/amenity_test.rb +23 -0
- data/test/hotels/caching_test.rb +42 -0
- data/test/hotels/ean_exception_test.rb +24 -0
- data/test/hotels/helpers_test.rb +52 -0
- data/test/hotels/hotel_location_test.rb +23 -0
- data/test/hotels/hotel_test.rb +112 -0
- data/test/hotels/image_test.rb +20 -0
- data/test/hotels/payment_option_test.rb +15 -0
- data/test/hotels/reservation_test.rb +15 -0
- data/test/hotels/room_test.rb +50 -0
- data/test/hotels/session_test.rb +14 -0
- data/test/keys.rb +43 -0
- data/test/minitest_helper.rb +29 -0
- data/test/support/fake_response.rb +13 -0
- metadata +220 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class NightlyRate
|
3
|
+
attr_accessor :promo, :rate, :base_rate
|
4
|
+
|
5
|
+
# Internal: Create a NightlyRate from the API response.
|
6
|
+
#
|
7
|
+
# info - A Hash from the API response containing nightly rate information.
|
8
|
+
def initialize(info)
|
9
|
+
@promo = info["@promo"]
|
10
|
+
@rate = info["@rate"]
|
11
|
+
@base_rate = info["@baseRate"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
class PaymentOption
|
4
|
+
attr_accessor :code, :name
|
5
|
+
|
6
|
+
extend Helpers
|
7
|
+
|
8
|
+
# Internal: Create a PaymentOption.
|
9
|
+
#
|
10
|
+
# code - The String code from the API response (e.g. "VI").
|
11
|
+
# name - The String name of the PaymentOption.
|
12
|
+
def initialize(code, name)
|
13
|
+
@code, @name = code, name
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Find PaymentOptions for a specific currency.
|
17
|
+
#
|
18
|
+
# info - A Hash of information with one key: :currency_code.
|
19
|
+
#
|
20
|
+
# Returns an Array of PaymentOption's.
|
21
|
+
def self.find(info)
|
22
|
+
params = { "currencyCode" => info[:currency_code] }
|
23
|
+
|
24
|
+
if Configuration.cache? and Configuration.cache.cached?(:paymentInfo, params)
|
25
|
+
types_raw = Configuration.cache.get_query(:paymentInfo, params)
|
26
|
+
else
|
27
|
+
types_raw = parse_response(url(:method => "paymentInfo", :params => params, :session => info[:session]))
|
28
|
+
Configuration.cache.save_query(:paymentInfo, params, types_raw) if Configuration.cache?
|
29
|
+
end
|
30
|
+
update_session(types_raw, info[:session])
|
31
|
+
|
32
|
+
types_raw["HotelPaymentResponse"].map do |raw|
|
33
|
+
types = raw[0] != "PaymentType" ? [] : raw[1]
|
34
|
+
types.map do |type|
|
35
|
+
PaymentOption.new(type["code"], type["name"])
|
36
|
+
end
|
37
|
+
end.flatten
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
class Reservation
|
4
|
+
attr_accessor :itinerary_id, :confirmation_numbers, :raw, :surcharges
|
5
|
+
|
6
|
+
# Internal: Create a new Reservation from the API response.
|
7
|
+
#
|
8
|
+
# info - The Hash of information returned from the API.
|
9
|
+
def initialize(info)
|
10
|
+
@itinerary_id, @confirmation_numbers = info[:itinerary_id], [info[:confirmation_numbers]].flatten
|
11
|
+
@surcharges = info[:surcharges]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
class Room
|
4
|
+
attr_accessor :rate_key, :hotel_id, :supplier_type, :rate_code,
|
5
|
+
:room_type_code, :supplier_type, :tax_rate, :non_refundable,
|
6
|
+
:occupancy, :quoted_occupancy, :min_guest_age, :total,
|
7
|
+
:surcharge_total, :nightly_rate_total, :average_base_rate,
|
8
|
+
:average_rate, :max_nightly_rate, :currency_code, :value_adds,
|
9
|
+
:room_type_description, :price_breakdown, :total_price,
|
10
|
+
:average_nightly_rate, :promo, :arrival, :departure, :rooms,
|
11
|
+
:bed_types, :cancellation_policy, :non_refundable,
|
12
|
+
:guarantee_required, :deposit_required, :surcharges,
|
13
|
+
:rate_description, :raw, :rate_change, :guarantee_only
|
14
|
+
|
15
|
+
extend Helpers
|
16
|
+
include Helpers
|
17
|
+
|
18
|
+
# Internal: Create a new Room from within a Room search query.
|
19
|
+
#
|
20
|
+
# info - A Hash of parsed information from the API, with any of the keys
|
21
|
+
# from the attr_accessor's list.
|
22
|
+
def initialize(info)
|
23
|
+
info.each do |k, v|
|
24
|
+
instance_variable_set("@" + k.to_s, v)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Reserve a room.
|
29
|
+
#
|
30
|
+
# info - A Hash of the information described on the Suitcase
|
31
|
+
# [wiki](http://github.com/thoughtfusion/suitcase/wiki/User-flow).
|
32
|
+
#
|
33
|
+
# Returns a Suitcase::Reservation.
|
34
|
+
def reserve!(info)
|
35
|
+
params = {}
|
36
|
+
params["hotelId"] = @hotel_id
|
37
|
+
params["arrivalDate"] = @arrival
|
38
|
+
params["departureDate"] = @departure
|
39
|
+
params["supplierType"] = @supplier_type
|
40
|
+
# Only submit the rateKey if it is a merchant hotel
|
41
|
+
params["rateKey"] = @rate_key if @supplier_type == "E"
|
42
|
+
params["rateTypeCode"] = @room_type_code
|
43
|
+
params["rateCode"] = @rate_code
|
44
|
+
params["roomTypeCode"] = @room_type_code
|
45
|
+
params["chargeableRate"] = chargeable_rate
|
46
|
+
params["email"] = info[:email]
|
47
|
+
params["firstName"] = info[:first_name]
|
48
|
+
params["lastName"] = info[:last_name]
|
49
|
+
params["homePhone"] = info[:home_phone]
|
50
|
+
params["workPhone"] = info[:work_phone] if info[:work_phone]
|
51
|
+
params["extension"] = info[:work_phone_extension] if info[:work_phone_extension]
|
52
|
+
params["faxPhone"] = info[:fax_phone] if info[:fax_phone]
|
53
|
+
params["companyName"] = info[:company_name] if info[:company_name]
|
54
|
+
if info[:additional_emails]
|
55
|
+
params["emailIntineraryList"] = info[:additional_emails].join(",")
|
56
|
+
end
|
57
|
+
params["creditCardType"] = info[:payment_option].code
|
58
|
+
params["creditCardNumber"] = info[:credit_card_number]
|
59
|
+
params["creditCardIdentifier"] = info[:credit_card_verification_code]
|
60
|
+
expiration_date = Date._parse(info[:credit_card_expiration_date])
|
61
|
+
params["creditCardExpirationMonth"] = if expiration_date[:mon] < 10
|
62
|
+
"0" + expiration_date[:mon].to_s
|
63
|
+
else
|
64
|
+
expiration_date[:mon].to_s
|
65
|
+
end
|
66
|
+
params["creditCardExpirationYear"] = expiration_date[:year].to_s
|
67
|
+
params["address1"] = info[:address1]
|
68
|
+
params["address2"] = info[:address2] if info[:address2]
|
69
|
+
params["address3"] = info[:address3] if info[:address3]
|
70
|
+
params["city"] = info[:city]
|
71
|
+
@rooms.each_with_index do |room, index|
|
72
|
+
index += 1
|
73
|
+
params["room#{index}"] = "#{room[:adults].to_s},#{room[:children_ages].join(",")}"
|
74
|
+
params["room#{index}FirstName"] = room[:first_name] || params["firstName"] # defaults to the billing
|
75
|
+
params["room#{index}LastName"] = room[:last_name] || params["lastName"] # person's name
|
76
|
+
params["room#{index}BedTypeId"] = room[:bed_type].id if @supplier_type == "E"
|
77
|
+
params["room#{index}SmokingPreference"] = room[:smoking_preference] || "E"
|
78
|
+
end
|
79
|
+
params["stateProvinceCode"] = info[:province]
|
80
|
+
params["countryCode"] = info[:country]
|
81
|
+
params["postalCode"] = info[:postal_code]
|
82
|
+
|
83
|
+
uri = Room.url(
|
84
|
+
method: "res",
|
85
|
+
params: params,
|
86
|
+
include_key: true,
|
87
|
+
include_cid: true,
|
88
|
+
secure: true
|
89
|
+
)
|
90
|
+
session = Patron::Session.new
|
91
|
+
session.timeout = 30000
|
92
|
+
session.base_url = "https://" + uri.host
|
93
|
+
res = session.post uri.request_uri, {}
|
94
|
+
parsed = JSON.parse res.body
|
95
|
+
|
96
|
+
reservation_res = parsed["HotelRoomReservationResponse"]
|
97
|
+
handle_errors(parsed)
|
98
|
+
rate_info = reservation_res["RateInfos"]["RateInfo"]
|
99
|
+
surcharges = if @supplier_type == "E" && rate_info["ChargeableRateInfo"]["Surcharges"]
|
100
|
+
[rate_info["ChargeableRateInfo"]["Surcharges"]["Surcharge"]].
|
101
|
+
flatten.map { |s| Surcharge.parse(s) }
|
102
|
+
else
|
103
|
+
[]
|
104
|
+
end
|
105
|
+
r = Reservation.new(
|
106
|
+
itinerary_id: reservation_res["itineraryId"],
|
107
|
+
confirmation_numbers: reservation_res["confirmationNumbers"],
|
108
|
+
surcharges: surcharges
|
109
|
+
)
|
110
|
+
r.raw = parsed
|
111
|
+
r
|
112
|
+
end
|
113
|
+
|
114
|
+
# Public: The chargeable rate for the Hotel room.
|
115
|
+
#
|
116
|
+
# Returns the chargeable rate for the Room.
|
117
|
+
def chargeable_rate
|
118
|
+
if @supplier_type == "E"
|
119
|
+
@total_price
|
120
|
+
else
|
121
|
+
@max_nightly_rate
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Public: The description of the displayed rate.
|
126
|
+
#
|
127
|
+
# Returns the rate description based on the `rate_change` attribute.
|
128
|
+
def room_rate_description
|
129
|
+
if @rate_change
|
130
|
+
"rate changes during the dates requested and the single nightly rate displayed is the highest nightly rate of the dates requested without taxes and fees."
|
131
|
+
else
|
132
|
+
"highest single night rate during the dates selected without taxes or fees"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Suitcase
|
2
|
+
class Hotel
|
3
|
+
# Public: A Surcharge represents a single surcharge on a Room.
|
4
|
+
class Surcharge
|
5
|
+
attr_accessor :amount, :type
|
6
|
+
# Internal: Create a new Surcharge.
|
7
|
+
#
|
8
|
+
# info - A Hash of parsed info from Surcharge.parse.
|
9
|
+
def initialize(info)
|
10
|
+
@amount, @type = info[:amount], info[:type]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Internal: Parse a Surcharge from the room response.
|
14
|
+
#
|
15
|
+
# info - A Hash of the parsed JSON relevant to the surhcarge.
|
16
|
+
#
|
17
|
+
# Returns a Surcharge representing the info.
|
18
|
+
def self.parse(info)
|
19
|
+
new(amount: info["@amount"], type: info["@type"])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "suitcase/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "rthbound-suitcase"
|
7
|
+
s.version = Suitcase::VERSION
|
8
|
+
s.authors = ["Walter Nelson", "Tad Hosford"]
|
9
|
+
s.email = ["tad@isotope11.com"]
|
10
|
+
s.homepage = "http://github.com/rthbound/suitcase"
|
11
|
+
s.summary = %q{Locates available hotels and rental cars through Expedia and Hotwire. This is a fork of Walter Nelson's gem - suitcase}
|
12
|
+
s.description = %q{Ruby library that utilizes the EAN (Expedia.com) API for locating available hotels and the Hotwire API for rental cars.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_development_dependency "minitest"
|
20
|
+
s.add_development_dependency "mocha"
|
21
|
+
s.add_development_dependency "turn"
|
22
|
+
|
23
|
+
s.add_development_dependency "chronic"
|
24
|
+
|
25
|
+
s.add_development_dependency "rake"
|
26
|
+
s.add_development_dependency "pry"
|
27
|
+
|
28
|
+
s.add_runtime_dependency "json"
|
29
|
+
s.add_runtime_dependency "patron"
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
describe Suitcase::CarRental do
|
4
|
+
before :each do
|
5
|
+
info = {
|
6
|
+
destination: "Seattle",
|
7
|
+
start_date: "07/04/2012",
|
8
|
+
end_date: "07/11/2012",
|
9
|
+
pickup_time: "07:30",
|
10
|
+
dropoff_time: "11:30"
|
11
|
+
}
|
12
|
+
@rentals = Suitcase::CarRental.find(info)
|
13
|
+
@rental = @rentals.first
|
14
|
+
end
|
15
|
+
|
16
|
+
[:seating, :type_name, :type_code, :possible_features,
|
17
|
+
:possible_models].each do |accessor|
|
18
|
+
it "has an accessor #{accessor}" do
|
19
|
+
@rental.must_respond_to(accessor)
|
20
|
+
@rental.must_respond_to((accessor.to_s + "=").to_sym)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe ".find" do
|
25
|
+
it "returns an Array of CarRental's" do
|
26
|
+
@rentals.must_be_kind_of(Array)
|
27
|
+
@rental.must_be_kind_of(Suitcase::CarRental)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
describe Suitcase::Hotel::Amenity do
|
4
|
+
describe ".parse_mask" do
|
5
|
+
describe "when provided bitmask is not nil or 0" do
|
6
|
+
it "returns an Array of Symbols representing given Amenities" do
|
7
|
+
Suitcase::Hotel::Amenity.parse_mask(5).must_equal [:business_services, :hot_tub]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "when bitmask is 0" do
|
12
|
+
it "returns an empty Array" do
|
13
|
+
Suitcase::Hotel::Amenity.parse_mask(0).must_equal []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "when provided bitmask is nil" do
|
18
|
+
it "returns nil" do
|
19
|
+
Suitcase::Hotel::Amenity.parse_mask(nil).must_equal nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
describe Suitcase do
|
4
|
+
before :each do
|
5
|
+
Suitcase::Configuration.cache = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
it "caches all non-secure queries" do
|
9
|
+
# Query 1
|
10
|
+
hotel = Suitcase::Hotel.find(id: 123904)
|
11
|
+
|
12
|
+
# Query 2
|
13
|
+
Suitcase::Hotel.find(location: "Boston, US")
|
14
|
+
|
15
|
+
# Query 3
|
16
|
+
room = hotel.rooms(arrival: Keys::RESERVATION_START_TIME, departure: Keys::RESERVATION_END_TIME).first
|
17
|
+
|
18
|
+
# Query 4
|
19
|
+
Suitcase::Hotel::PaymentOption.find currency_code: "USD"
|
20
|
+
|
21
|
+
# Query 5, don't cache though
|
22
|
+
room.rooms[0][:bed_type] = room.bed_types[0]
|
23
|
+
room.rooms[0][:smoking_preference] = "NS"
|
24
|
+
room.reserve!(Keys::VALID_RESERVATION_INFO)
|
25
|
+
|
26
|
+
Suitcase::Configuration.cache.keys.count.must_equal 4
|
27
|
+
end
|
28
|
+
|
29
|
+
it "retrieves from the cache if it's there" do
|
30
|
+
hotel = Suitcase::Hotel.find(id: 123904)
|
31
|
+
Suitcase::Hotel.find(location: "Boston, US")
|
32
|
+
hotel.rooms(arrival: Keys::RESERVATION_START_TIME, departure: Keys::RESERVATION_END_TIME)
|
33
|
+
Suitcase::Hotel::PaymentOption.find currency_code: "USD"
|
34
|
+
|
35
|
+
# Disable API access
|
36
|
+
Net::HTTP.expects(:get_response).never
|
37
|
+
hotel = Suitcase::Hotel.find(id: 123904)
|
38
|
+
Suitcase::Hotel.find(location: "Boston, US")
|
39
|
+
hotel.rooms(arrival: Keys::RESERVATION_START_TIME, departure: Keys::RESERVATION_END_TIME)
|
40
|
+
Suitcase::Hotel::PaymentOption.find currency_code: "USD"
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
describe Suitcase::Hotel::EANException do
|
4
|
+
before :each do
|
5
|
+
@exception = Suitcase::Hotel::EANException.new(nil)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "has an accessor recovery" do
|
9
|
+
@exception.must_respond_to(:recovery)
|
10
|
+
@exception.must_respond_to(:recovery=)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#recoverable" do
|
14
|
+
it "returns true if the recovery attribute is set" do
|
15
|
+
@exception.recovery = { locations: ["London", "New London"] }
|
16
|
+
@exception.recoverable?.must_equal(true)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns false if the recovery attribute is not set" do
|
20
|
+
@exception.recovery = nil
|
21
|
+
@exception.recoverable?.must_equal(false)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "minitest_helper"
|
2
|
+
|
3
|
+
describe Suitcase::Hotel::Helpers do
|
4
|
+
before :each do
|
5
|
+
class Dummy; extend Suitcase::Hotel::Helpers; end
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#url" do
|
9
|
+
it "returns a URI with the proper base URL" do
|
10
|
+
url = Dummy.url(method: "action", params: {})
|
11
|
+
url.must_be_kind_of(URI)
|
12
|
+
url.host.must_match(/api.ean.com/)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "when using digital signature authorization" do
|
16
|
+
it "adds a 'sig' parameter" do
|
17
|
+
Suitcase::Configuration.expects(:use_signature_auth?).returns(true)
|
18
|
+
Dummy.expects(:generate_signature).returns("test")
|
19
|
+
|
20
|
+
url = Dummy.url(method: "action", params: {})
|
21
|
+
url.query.must_match(/sig=test/)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#parse_response" do
|
27
|
+
it "raises an exception when passed an invalid URI" do
|
28
|
+
proc do
|
29
|
+
Dummy.parse_response(URI.parse("http://google.com"))
|
30
|
+
end.must_raise JSON::ParserError
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raises an error if a 403 code is received" do
|
34
|
+
proc do
|
35
|
+
response = FakeResponse.new(code: 403, body: "<h1>An error occurred.</h1>")
|
36
|
+
Net::HTTP.stubs(:get_response).returns(response)
|
37
|
+
Dummy.parse_response(URI.parse("http://fake.response.will.be.used"))
|
38
|
+
end.must_raise Suitcase::Hotel::EANException
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#generate_signature" do
|
43
|
+
it "returns the encrypted API key, shared secret, and timestamp" do
|
44
|
+
Suitcase::Configuration.expects(:hotel_api_key).returns("abc")
|
45
|
+
Suitcase::Configuration.expects(:hotel_shared_secret).returns("123")
|
46
|
+
Time.expects(:now).returns("10")
|
47
|
+
|
48
|
+
signature = Dummy.generate_signature
|
49
|
+
signature.must_equal(Digest::MD5.hexdigest("abc12310"))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|