parsec_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/parsec.rb +17 -0
- data/lib/parsec/availability.rb +113 -0
- data/lib/parsec/base.rb +7 -0
- data/lib/parsec/city.rb +13 -0
- data/lib/parsec/configuration.rb +12 -0
- data/lib/parsec/country.rb +13 -0
- data/lib/parsec/hotel.rb +159 -0
- data/lib/parsec/location.rb +14 -0
- data/lib/parsec/order.rb +40 -0
- data/lib/parsec/region.rb +13 -0
- data/lib/parsec/request/availability.rb +70 -0
- data/lib/parsec/request/base.rb +65 -0
- data/lib/parsec/request/city.rb +18 -0
- data/lib/parsec/request/country.rb +18 -0
- data/lib/parsec/request/hotel.rb +46 -0
- data/lib/parsec/request/location.rb +19 -0
- data/lib/parsec/request/order.rb +134 -0
- data/lib/parsec/request/region.rb +18 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 79dc7741365756dd9fd8286225be1d30811cf500f22eff258a20a06eeee9f322
|
4
|
+
data.tar.gz: 5d5e78a36f30c0f6e88a29f6bbc7d9479ba354070b4599012aa61db58cac13d0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e2ff815e3b14c501d91b40c915a98377608a3c66a8ab3cce8a75e8b914742ebf9103f4bcd62794f971b1cb46b1a03f7487a8c3cdbeecc740bbe368d0f9f89d07
|
7
|
+
data.tar.gz: 474d1559f3b861f137e983e9cbfecb5607398ae09e25408ff5773cb6128acaa63e9d1f36d278ac7484cb3761218bcba5821f0ad7efc30f0bc87cf675a55637b1
|
data/lib/parsec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Parsec
|
2
|
+
class << self
|
3
|
+
attr_accessor :configuration
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.configuration
|
7
|
+
@configuration ||= Configuration.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.reset
|
11
|
+
@configuration = Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
yield configuration
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
class Availability < Base
|
5
|
+
MEAL_PLANS = {
|
6
|
+
'RO' => 'Room Only',
|
7
|
+
'BB' => 'Breakfast is included',
|
8
|
+
'HB' => 'Half board (two meals) included',
|
9
|
+
'FB' => 'Full board (three meals) included',
|
10
|
+
'AI' => 'All Inclusive'
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
PACKAGE_RATES_CODE = 'PK'
|
14
|
+
|
15
|
+
attr_reader :hotel_code, :hotel_provider_code, :room_code, :room_name, :rates, :rph, :description, :extra, :special
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/ParameterLists
|
18
|
+
def initialize(hotel_code:, hotel_provider_code: nil, room_code: nil, room_name: nil, rates:,
|
19
|
+
rph: nil, description: nil, extra: nil, special: nil, package: false)
|
20
|
+
@hotel_code = hotel_code
|
21
|
+
@hotel_provider_code = hotel_provider_code
|
22
|
+
@room_code = room_code
|
23
|
+
@room_name = room_name
|
24
|
+
@package = package
|
25
|
+
@rates = rates
|
26
|
+
@rph = rph
|
27
|
+
@description = description
|
28
|
+
@extra = extra
|
29
|
+
@special = special
|
30
|
+
end
|
31
|
+
# rubocop:enable Metrics/ParameterLists
|
32
|
+
|
33
|
+
def package_rates?
|
34
|
+
@package
|
35
|
+
end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
# rubocop:disable Metrics/AbcSize
|
39
|
+
def build(room, hotel_info, nights)
|
40
|
+
new(hotel_code: hotel_info[:@hotel_code],
|
41
|
+
hotel_provider_code: hotel_info[:hotel_provider_code],
|
42
|
+
room_code: room[:room_type][:@code],
|
43
|
+
room_name: room[:room_type][:@name],
|
44
|
+
package: package_rates?(room),
|
45
|
+
description: room[:room_type][:description],
|
46
|
+
extra: room[:room_type].dig(:extra_info, :msg),
|
47
|
+
special: room[:room_type][:special],
|
48
|
+
rates: Array.wrap(room[:room_rates][:room_rate]).map { |rate| build_rate(rate, nights) },
|
49
|
+
rph: room[:@rph].split(',').first)
|
50
|
+
end
|
51
|
+
# rubocop:enable Metrics/AbcSize
|
52
|
+
|
53
|
+
def search(room, hotel_code)
|
54
|
+
new(hotel_code: hotel_code,
|
55
|
+
package: package_rates?(room),
|
56
|
+
rates: Array.wrap(room[:room_rates][:room_rate]).map { |rate| build_rate(rate) })
|
57
|
+
end
|
58
|
+
|
59
|
+
# rubocop:disable Metrics/AbcSize, Metrics/LineLength
|
60
|
+
def build_rate(rate, nights = 1)
|
61
|
+
price = rate[:total][:@amount].to_f.ceil
|
62
|
+
{ price: price,
|
63
|
+
price_per_night: (price.to_f / nights).round(2),
|
64
|
+
net_price: rate[:cost][:@amount].to_f,
|
65
|
+
cancellation: build_customer_cancellation(rate[:cancel_penalties], Array.wrap(rate[:rates][:rate])[0][:@effective_date]),
|
66
|
+
penalties: build_cancellation(rate[:cancel_penalties]),
|
67
|
+
deadline: build_deadline_date(rate[:cancel_penalties], Array.wrap(rate[:rates][:rate])[0][:@effective_date]),
|
68
|
+
meal_plan: MEAL_PLANS[rate[:@meal_plan]],
|
69
|
+
booking_code: rate[:@booking_code] }
|
70
|
+
end
|
71
|
+
# rubocop:enable Metrics/AbcSize, Metrics/LineLength
|
72
|
+
|
73
|
+
def build_cancellation(cancellation)
|
74
|
+
# return if cancellation.nil?
|
75
|
+
return 'Non-refundable rate' if cancellation[:@non_refundable] == '1'
|
76
|
+
|
77
|
+
penalties = Array.wrap(cancellation[:cancel_penalty]).map { |p| build_penalty(p) }
|
78
|
+
penalties << cancellation[:description] if cancellation[:description].present?
|
79
|
+
|
80
|
+
penalties.join(' ')
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_penalty(penalty)
|
84
|
+
p = [penalty[:deadline][:@units], penalty[:deadline][:@time_unit], '=', "$#{penalty[:charge][:@amount]};"]
|
85
|
+
p << "(#{penalty[:msg]})" if penalty[:msg].present?
|
86
|
+
p.join(' ')
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_customer_cancellation(cancellation, start_date)
|
90
|
+
# return if cancellation.nil?
|
91
|
+
return 'Special non-refundable rate' if cancellation.slice(:@non_refundable, :@cancellation_costs_today).value?('1')
|
92
|
+
|
93
|
+
deadline = Array.wrap(cancellation[:cancel_penalty]).max_by { |p| p[:deadline][:@units].to_i }
|
94
|
+
return 'Free cancellation' if deadline.nil?
|
95
|
+
|
96
|
+
deadline_date = Date.strptime(start_date, self::DATE_FORMAT) - (deadline[:deadline][:@units].to_i + 1).days
|
97
|
+
|
98
|
+
"Free cancellation until #{deadline_date.strftime('%-d %B')} at 12:00PM, local time"
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_deadline_date(cancellation, start_date)
|
102
|
+
return Time.zone.today if cancellation[:@non_refundable] == '1'
|
103
|
+
|
104
|
+
deadline = Array.wrap(cancellation[:cancel_penalty]).max_by { |p| p[:deadline][:@units].to_i }
|
105
|
+
Date.strptime(start_date, self::DATE_FORMAT) - (deadline&.dig(:deadline, :@units).to_i + 1).days
|
106
|
+
end
|
107
|
+
|
108
|
+
def package_rates?(room)
|
109
|
+
room[:room_type][:@product_type_info1] == PACKAGE_RATES_CODE
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/parsec/base.rb
ADDED
data/lib/parsec/city.rb
ADDED
data/lib/parsec/hotel.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
class Hotel < Base
|
5
|
+
attr_reader :code
|
6
|
+
attr_accessor :email, :phone, :fax, :location, :check_in, :check_out, :name, :rating, :address, :parsec_region_id,
|
7
|
+
:hotel_facilities, :room_facilities, :entertainments, :points_of_interest, :additional_info, :facts,
|
8
|
+
:parsec_location_id, :descriptions, :hotel_url, :images, :error
|
9
|
+
|
10
|
+
def initialize(code:, name: nil, rating: nil, address: nil)
|
11
|
+
@code = code
|
12
|
+
@name = name
|
13
|
+
@rating = rating
|
14
|
+
@address = address
|
15
|
+
@hotel_facilities = {}
|
16
|
+
@room_facilities = {}
|
17
|
+
@entertainments = {}
|
18
|
+
@additional_info = {}
|
19
|
+
@facts = {}
|
20
|
+
@points_of_interest = {}
|
21
|
+
@descriptions = {}
|
22
|
+
@images = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.build(hotel_json)
|
26
|
+
hotel = new(code: hotel_json[:@hotel_code],
|
27
|
+
name: hotel_json[:@hotel_name],
|
28
|
+
rating: hotel_json[:award][:@rating])
|
29
|
+
# rooms: []) doesn't work now - this option has to be enabled on Parsec side
|
30
|
+
hotel.add_address(hotel_json[:address])
|
31
|
+
hotel.add_phones(Array.wrap(hotel_json.dig(:contact_numbers, :contact_number)))
|
32
|
+
hotel.email = hotel_json.dig(:tpa_extensions, :email)
|
33
|
+
|
34
|
+
hotel
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_details(details_json)
|
38
|
+
return add_error if details_json[:@hotel_name].blank?
|
39
|
+
|
40
|
+
# self.name ||= details_json[:@hotel_name]
|
41
|
+
self.parsec_region_id ||= details_json[:@region_code]
|
42
|
+
self.parsec_location_id ||= details_json[:@location_code]
|
43
|
+
add_hotel_info(details_json[:hotel_info])
|
44
|
+
add_policy(details_json.dig(:policies, :policy, :policy_info))
|
45
|
+
add_contact_info(details_json[:contact_infos][:contact_info])
|
46
|
+
add_extentions(details_json[:tpa_extensions])
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_hotel_info(hotel_info)
|
50
|
+
self.rating ||= hotel_info[:category_codes][:hotel_category][:@code]
|
51
|
+
# self.descriptions = Array.wrap(hotel_info.dig(:descriptions, :descriptive_text))
|
52
|
+
add_location(hotel_info[:position])
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_location(position)
|
56
|
+
self.location = { lat: position[:@latitude], lon: position[:@longitude] }
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_policy(policy)
|
60
|
+
return if policy.blank?
|
61
|
+
|
62
|
+
self.check_in = policy[:@check_in_time]
|
63
|
+
check_out_data = policy[:@check_out_time]
|
64
|
+
self.check_out = check_out_data.index('time').nil? ? check_out_data : JSON.parse(check_out_data)['time']
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_contact_info(contact_info)
|
68
|
+
return if contact_info.blank?
|
69
|
+
|
70
|
+
self.email ||= contact_info.dig(:emails, :email)
|
71
|
+
# self.images = Array.wrap(contact_info.dig(:ur_ls, :url))
|
72
|
+
add_address(contact_info.dig(:addresses, :address))
|
73
|
+
add_phones(Array.wrap(contact_info.dig(:phones, :phone)))
|
74
|
+
end
|
75
|
+
|
76
|
+
FACTS_WHITE_LIST = %w[Kind Floors\ Number Rooms\ Number Year\ Built Year\ Renovated].freeze
|
77
|
+
|
78
|
+
# rubocop:disable Metrics/AbcSize, Metrics/LineLength
|
79
|
+
def add_extentions(extentions)
|
80
|
+
return if extentions.blank?
|
81
|
+
|
82
|
+
self.hotel_facilities = Array.wrap(extentions.dig(:hotel_facilities, :hotel_facility)).map { |f| transform_facility(f) }.to_h
|
83
|
+
self.room_facilities = Array.wrap(extentions.dig(:room_facilities, :room_facility)).map { |f| transform_facility(f) }.to_h
|
84
|
+
self.entertainments = Array.wrap(extentions.dig(:entertainments, :entertainment)).map { |f| transform_facility(f) }.to_h
|
85
|
+
self.additional_info = Array.wrap(extentions.dig(:additional_infos, :additional_info)).map { |f| transform_facility(f) }.to_h
|
86
|
+
self.facts = Array.wrap(extentions.dig(:facts, :fact)).map do |f|
|
87
|
+
ff = f[:@name].split(': ', 2)
|
88
|
+
ff.size == 1 ? ff << 'null' : ff
|
89
|
+
end.to_h.slice(*FACTS_WHITE_LIST)
|
90
|
+
self.points_of_interest = Array.wrap(extentions.dig(:points_of_interest, :point_of_interest))
|
91
|
+
.sort_by { |f| f[:@distance].to_i }.map { |f| transform_facility(f) }.to_h
|
92
|
+
end
|
93
|
+
# rubocop:enable Metrics/AbcSize, Metrics/LineLength
|
94
|
+
|
95
|
+
def transform_facility(facility)
|
96
|
+
[facility[:@name], facility.except(:@name).transform_keys { |k| k.to_s.delete('@') }]
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_phones(contact_numbers)
|
100
|
+
return if contact_numbers.blank?
|
101
|
+
|
102
|
+
phone_number = find_phone(contact_numbers)
|
103
|
+
fax_number = find_fax(contact_numbers)
|
104
|
+
self.phone = phone_number if phone_number.present?
|
105
|
+
self.fax = fax_number if fax_number.present?
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_address(address)
|
109
|
+
return if address.blank?
|
110
|
+
return if address[:address_line].blank?
|
111
|
+
|
112
|
+
self.address = [address[:address_line], address[:city_name], address[:country_name]].join(', ')
|
113
|
+
end
|
114
|
+
|
115
|
+
def find_phone(contact_numbers)
|
116
|
+
phone = contact_numbers.find { |number| number[:@phone_tech_type] == 'Phone' }
|
117
|
+
phone&.dig(:@phone_number)
|
118
|
+
end
|
119
|
+
|
120
|
+
def find_fax(contact_numbers)
|
121
|
+
fax = contact_numbers.find { |number| number[:@phone_tech_type] == 'Fax' }
|
122
|
+
fax&.dig(:@phone_number)
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_error
|
126
|
+
self.error = 'Parsec doesn\'t provide hotel details'
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_details_with_nokogiri(document)
|
130
|
+
info = Nokogiri::XML.parse(document.first_element_child
|
131
|
+
.last_element_child
|
132
|
+
.last_element_child
|
133
|
+
.last_element_child
|
134
|
+
.last_element_child.to_xml)
|
135
|
+
|
136
|
+
self.name = info.search('HotelDescriptiveContent').first.attributes['HotelName'].value
|
137
|
+
|
138
|
+
add_descriptions(info)
|
139
|
+
add_images(info)
|
140
|
+
end
|
141
|
+
|
142
|
+
def add_descriptions(info)
|
143
|
+
info.search('DescriptiveText').each do |desc|
|
144
|
+
title = desc.values.first.split(/(?=[A-Z])/).join(' ')
|
145
|
+
descriptions[title] = desc.content
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def add_images(info)
|
150
|
+
info.search('URL').each do |url|
|
151
|
+
if url.values.first == 'Image'
|
152
|
+
images << url.content
|
153
|
+
elsif url.values.first == 'Hotel'
|
154
|
+
self.hotel_url = url.content
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
class Location < Base
|
5
|
+
attr_reader :code, :name, :country_iso
|
6
|
+
|
7
|
+
def initialize(code:, name: nil, city_code: nil, country_iso: nil)
|
8
|
+
@code = code
|
9
|
+
@name = name
|
10
|
+
@city_code = city_code
|
11
|
+
@country_iso = country_iso
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/parsec/order.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
class Order < Base
|
5
|
+
RES_STATUS = {
|
6
|
+
'OK' => 'Success', # 'New booking'
|
7
|
+
'CA' => 'Cancelled',
|
8
|
+
'OR' => 'On Request'
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
attr_reader :number, :rooms, :price, :hotel_code, :start_date, :end_date, :parsec_id, :status
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
14
|
+
def initialize(product_item_id:, rooms:, price:, hotel_code:, start_date:, end_date:, parsec_id:, status:)
|
15
|
+
@product_item_id = product_item_id
|
16
|
+
@rooms = rooms
|
17
|
+
@price = price
|
18
|
+
@hotel_code = hotel_code
|
19
|
+
@start_date = start_date
|
20
|
+
@end_date = end_date
|
21
|
+
@parsec_id = parsec_id
|
22
|
+
@status = status
|
23
|
+
end
|
24
|
+
# rubocop:enable Metrics/ParameterLists
|
25
|
+
|
26
|
+
# rubocop:disable Metrics/AbcSize
|
27
|
+
def self.build(order_data)
|
28
|
+
hotel_data = order_data[:hotel_res_list][:hotel_res]
|
29
|
+
new(product_item_id: order_data[:res_global_info][:res_i_ds][:res_id].find { |id| id[:@type] == 'ClientReference' }[:@id],
|
30
|
+
rooms: Array.wrap(hotel_data[:rooms][:room]).map { |r| [r[:room_type][:@code], r[:room_type][:@name]] }.to_h,
|
31
|
+
hotel_code: hotel_data[:info][:@hotel_code],
|
32
|
+
start_date: Date.strptime(hotel_data[:hotel_res_info][:date_range][:@start], DATE_FORMAT),
|
33
|
+
end_date: Date.strptime(hotel_data[:hotel_res_info][:date_range][:@end], DATE_FORMAT),
|
34
|
+
price: hotel_data[:hotel_res_info][:total][:@amount].to_f,
|
35
|
+
parsec_id: hotel_data[:hotel_res_info][:hotel_res_i_ds][:hotel_res_id].find { |id| id[:@type] == 'Locator' }[:@id],
|
36
|
+
status: RES_STATUS[hotel_data[:@res_status]])
|
37
|
+
end
|
38
|
+
# rubocop:enable Metrics/AbcSize
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class Availability < Base
|
6
|
+
# rooms - array of room hashes, e.g. { rph1: { adults: 2, children: [5, 8]}, rph2: { adults: 3 }}
|
7
|
+
# rubocop:disable Metrics/AbcSize
|
8
|
+
def by_hotel(from_date, to_date, rooms, hotel_search_criteria)
|
9
|
+
hotels = availability_request(from_date, to_date, rooms, hotel_search_criteria)
|
10
|
+
return error(hotels[:ota_hotel_avail_rs]) if hotels[:ota_hotel_avail_rs][:errors].present?
|
11
|
+
|
12
|
+
Array.wrap(hotels[:ota_hotel_avail_rs][:hotels][:hotel]).map do |h|
|
13
|
+
Array.wrap(h[:rooms][:room]).map { |r| Parsec::Availability.build(r, h[:info], (to_date - from_date).to_i) }
|
14
|
+
end.flatten.group_by(&:hotel_code)
|
15
|
+
end
|
16
|
+
|
17
|
+
def search(from_date, to_date, rooms, search_criteria)
|
18
|
+
hotels = availability_request(from_date, to_date, rooms, search_criteria)
|
19
|
+
return error(hotels[:ota_hotel_avail_rs]) if hotels[:ota_hotel_avail_rs][:errors].present?
|
20
|
+
|
21
|
+
Array.wrap(hotels[:ota_hotel_avail_rs][:hotels][:hotel]).map do |h|
|
22
|
+
Array.wrap(h[:rooms][:room]).map { |r| Parsec::Availability.search(r, h[:info][:@hotel_code]) }
|
23
|
+
end.flatten.group_by(&:hotel_code)
|
24
|
+
end
|
25
|
+
# rubocop:enable Metrics/AbcSize
|
26
|
+
|
27
|
+
def availability_request(from_date, to_date, rooms, search_criteria)
|
28
|
+
attributes = { 'RateDetails' => '1', 'DetailLevel' => '1', 'CancelPenalties' => '1', 'Language' => 'EN' }
|
29
|
+
message = message(from_date, to_date, rooms_description(rooms), search_criteria)
|
30
|
+
response = client(:availability).call('OTA_HotelAvailRQ', attributes: attributes, message: message)
|
31
|
+
|
32
|
+
response.body
|
33
|
+
rescue Savon::Error => e
|
34
|
+
Xlog.and_raise_error(e)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def message(from_date, to_date, rooms, search_criteria)
|
40
|
+
{
|
41
|
+
hotel_search: {
|
42
|
+
# '@BestOnly' => '1',
|
43
|
+
'@AvailableOnly' => '1',
|
44
|
+
# '@FilterNonRefundable' => '1',
|
45
|
+
# '@FilterCancellationCostsToday' => '1',
|
46
|
+
currency: { '@Code' => 'USD' },
|
47
|
+
date_range: {
|
48
|
+
'@Start' => from_date.strftime(DATE_FORMAT),
|
49
|
+
'@End' => to_date.strftime(DATE_FORMAT)
|
50
|
+
},
|
51
|
+
room_candidates: { room_candidate: rooms }
|
52
|
+
}.merge!(search_criteria)
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def rooms_description(rooms)
|
57
|
+
rooms.map do |rph, room_guests|
|
58
|
+
{ '@RPH' => rph, guests: { guest: guests_description(room_guests.symbolize_keys) } }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def guests_description(adults:, children: nil)
|
63
|
+
guests = [{ '@AgeCode' => 'A', '@Count' => adults }]
|
64
|
+
children.each { |c| guests << { '@AgeCode' => 'C', '@Count' => 1, '@Age' => c } } if children.present?
|
65
|
+
|
66
|
+
guests
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class Base
|
6
|
+
RESOURCES = {
|
7
|
+
static_data: 'staticdata/OTA2014A',
|
8
|
+
hotel_info: 'hotelinfo/OTA2014A',
|
9
|
+
availability: 'hotelavail/OTA2014Compact',
|
10
|
+
reservation: 'hotelres/OTA2014Compact',
|
11
|
+
list: 'bookinglist/OTA2014Compact',
|
12
|
+
read: 'reservationsread/OTA2014Compact',
|
13
|
+
cancel: 'hotelcancel/OTA2014Compact'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
NAMESPACE = 'http://parsec.es/hotelapi/OTA2014Compact'
|
17
|
+
|
18
|
+
DATE_FORMAT = '%Y-%m-%d'
|
19
|
+
|
20
|
+
def initialize(integration = nil)
|
21
|
+
@integration = integration
|
22
|
+
end
|
23
|
+
|
24
|
+
def client(endpoint)
|
25
|
+
Savon.client endpoint: "#{Parsec.configuration.host}/NewAvailabilityServlet/#{RESOURCES[endpoint]}",
|
26
|
+
namespace: NAMESPACE,
|
27
|
+
convert_request_keys_to: :camelcase,
|
28
|
+
soap_header: security_tag
|
29
|
+
end
|
30
|
+
|
31
|
+
def error(response)
|
32
|
+
{ error: response[:errors][:error] }
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def security_tag
|
38
|
+
{
|
39
|
+
'wsse:Security' => {
|
40
|
+
'@xmlns:wsse' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
|
41
|
+
'wsse:Username' => username,
|
42
|
+
'wsse:Password' => password,
|
43
|
+
'Context' => context
|
44
|
+
}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def username
|
49
|
+
@integration&.parsec_username.presence || Parsec.configuration.username
|
50
|
+
end
|
51
|
+
|
52
|
+
def password
|
53
|
+
@integration&.parsec_password.presence || Parsec.configuration.password
|
54
|
+
end
|
55
|
+
|
56
|
+
def context
|
57
|
+
Parsec.configuration.context
|
58
|
+
end
|
59
|
+
|
60
|
+
def logger
|
61
|
+
@my_logger ||= Logger.new("#{Rails.root}/log/parsec.log")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class City < Base
|
6
|
+
def by_country(country_iso_code)
|
7
|
+
message = { read_request: { hotel_read_request: { request_type: 'GetCities', country_code: country_iso_code } } }
|
8
|
+
response = client(:static_data).call('OTA_ReadRQ', message: message)
|
9
|
+
|
10
|
+
Array.wrap(response.body.dig(:ota_read_rs, :read_response, :cities, :city)).map do |c|
|
11
|
+
Parsec::City.new(code: c[:@city_code], name: c[:city_name], country_iso: c[:country_iso])
|
12
|
+
end
|
13
|
+
rescue Savon::Error => e
|
14
|
+
Xlog.and_raise_error(e)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class Country < Base
|
6
|
+
def all
|
7
|
+
message = { read_request: { hotel_read_request: { request_type: 'GetCountries' } } }
|
8
|
+
response = client(:static_data).call('OTA_ReadRQ', message: message)
|
9
|
+
|
10
|
+
response.body[:ota_read_rs][:read_response][:countries][:country] do |c|
|
11
|
+
Parsec::Country.new(iso: c[:country_iso], name: c[:country_name], code: c[:@country_code])
|
12
|
+
end
|
13
|
+
rescue Savon::Error => e
|
14
|
+
Xlog.and_raise_error(e)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class Hotel < Base
|
6
|
+
def details(code)
|
7
|
+
message = { hotel_descriptive_infos: { '@LangRequested' => 'EN', hotel_descriptive_info: { '@HotelCode' => code } } }
|
8
|
+
response = client(:hotel_info).call('OTA_HotelDescriptiveInfoRQ', message: message)
|
9
|
+
|
10
|
+
hotel = Parsec::Hotel.new(code: code)
|
11
|
+
hotel.add_details(response.body[:ota_hotel_descriptive_info_rs][:hotel_descriptive_contents][:hotel_descriptive_content])
|
12
|
+
hotel.add_details_with_nokogiri(response.doc)
|
13
|
+
|
14
|
+
hotel
|
15
|
+
rescue Savon::Error => e
|
16
|
+
Xlog.and_raise_error(e)
|
17
|
+
end
|
18
|
+
|
19
|
+
def by_country(country_iso_code)
|
20
|
+
hotels = hotel_search_request(ref_point: { '@CountryCode' => country_iso_code })
|
21
|
+
hotels.map { |h| Parsec::Hotel.build(h) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def by_city(city_code)
|
25
|
+
hotels = hotel_search_request(hotel_ref: { '@HotelCityCode' => city_code })
|
26
|
+
hotels.map { |h| Parsec::Hotel.build(h) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch(hotel_code)
|
30
|
+
hotels = hotel_search_request(hotel_ref: { '@HotelCode' => hotel_code })
|
31
|
+
Parsec::Hotel.build(hotels.first)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def hotel_search_request(search_criterion)
|
37
|
+
message = { criteria: { criterion: search_criterion } }
|
38
|
+
# message = { criteria: { criterion: search_criterion.merge!('TPA_Extensions' => { return_rooms: true }) } }
|
39
|
+
response = client(:static_data).call('OTA_HotelSearchRQ', message: message, response_parser: :rexml)
|
40
|
+
Array.wrap(response.body.dig(:ota_hotel_search_rs, :properties, :property))
|
41
|
+
rescue Savon::Error => e
|
42
|
+
Xlog.and_raise_error(e)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class Location < Base
|
6
|
+
def by_city(city_code)
|
7
|
+
message = { read_request: { hotel_read_request: { request_type: 'GetLocations', city_code: city_code } } }
|
8
|
+
response = client(:static_data).call('OTA_ReadRQ', message: message)
|
9
|
+
|
10
|
+
Array.wrap(response.body.dig(:ota_read_rs, :read_response, :locations, :location)).map do |l|
|
11
|
+
Parsec::Location.new(code: l[:@location_code], name: l[:location_name],
|
12
|
+
city_code: l[:city_code], country_iso: l[:country_iso])
|
13
|
+
end
|
14
|
+
rescue Savon::Error => e
|
15
|
+
Xlog.and_raise_error(e)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class Order < Base
|
6
|
+
def list(from_date, to_date, hotel_code = nil)
|
7
|
+
attributes = { xmlns: NAMESPACE }
|
8
|
+
booking_search = {
|
9
|
+
date_range: {
|
10
|
+
'@Start' => from_date.strftime(DATE_FORMAT),
|
11
|
+
'@End' => to_date.strftime(DATE_FORMAT),
|
12
|
+
'@DateType' => 'Arrival'
|
13
|
+
}
|
14
|
+
}
|
15
|
+
booking_search[:hotel_ref] = { '@HotelCode' => hotel_code } if hotel_code.present?
|
16
|
+
response = client(:list).call('OTA_BookingListRQ', attributes: attributes, message: { booking_search: booking_search })
|
17
|
+
return error(response.body[:ota_booking_list_rs]) if response.body[:ota_booking_list_rs][:errors].present?
|
18
|
+
|
19
|
+
response.body[:ota_booking_list_rs]
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_booking(booking_data)
|
23
|
+
message = { hotel_res: { rooms: { room: rooms(booking_data) } } }
|
24
|
+
response = booking_request('PreBooking', message)
|
25
|
+
return error(response[:ota_booking_info_rs]) if response[:ota_booking_info_rs][:errors].present?
|
26
|
+
|
27
|
+
{ success: response[:ota_booking_info_rs][:success] }
|
28
|
+
end
|
29
|
+
|
30
|
+
# booking_data = [{ booking_code1:, adults1:, children1: }, { booking_code2:, adults2:, children2: }, ...]
|
31
|
+
def create(booking_data, client_id, customer_text = nil)
|
32
|
+
message = {
|
33
|
+
unique_i_d: { '@Type' => 'ClientReference', '@ID' => client_id },
|
34
|
+
hotel_res: { rooms: { room: rooms_with_guests(booking_data, client_id) } }
|
35
|
+
}
|
36
|
+
message[:hotel_res][:special_requests] = { text: customer_text } if customer_text.present?
|
37
|
+
response = booking_request('Booking', message)
|
38
|
+
return error(response[:ota_booking_info_rs]) if response[:ota_booking_info_rs][:errors].present?
|
39
|
+
|
40
|
+
Parsec::Order.build(response[:ota_booking_info_rs])
|
41
|
+
end
|
42
|
+
|
43
|
+
def read(parsec_id)
|
44
|
+
attributes = { xmlns: NAMESPACE, 'DetailLevel' => '1', 'RateDetails' => '1', 'Language' => 'EN' }
|
45
|
+
message = { unique_i_d: { '@Type' => 'Locator', '@ID' => parsec_id } }
|
46
|
+
response = client(:read).call('OTA_ReadRQ', attributes: attributes, message: message)
|
47
|
+
return error(response.body[:ota_booking_info_rs]) if response.body[:ota_booking_info_rs][:errors].present?
|
48
|
+
|
49
|
+
Parsec::Order.build(response.body[:ota_booking_info_rs])
|
50
|
+
rescue Savon::Error => e
|
51
|
+
Xlog.and_raise_error(e)
|
52
|
+
end
|
53
|
+
|
54
|
+
def pre_cancel(parsec_id)
|
55
|
+
response = cancel_request('PreCancel', parsec_id)
|
56
|
+
return error(response[:ota_cancel_rs]) if response.dig(:ota_cancel_rs, :errors).present?
|
57
|
+
|
58
|
+
{ fee_amount: response[:ota_cancel_rs][:cancel_info_rs][:cancellation_costs][:@amount].to_f }
|
59
|
+
end
|
60
|
+
|
61
|
+
def cancel(parsec_id)
|
62
|
+
response = cancel_request('Cancel', parsec_id)
|
63
|
+
return error(response[:ota_cancel_rs]) if response.dig(:ota_cancel_rs, :errors).present?
|
64
|
+
|
65
|
+
Parsec::Order.build(response[:ota_booking_info_rs])
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def booking_request(type, message)
|
71
|
+
attributes = { xmlns: NAMESPACE, 'Transaction' => type, 'DetailLevel' => '1', 'RateDetails' => '1', 'Language' => 'EN' }
|
72
|
+
response = client(:reservation).call('OTA_HotelResRQ', attributes: attributes, message: message)
|
73
|
+
if type == 'Booking'
|
74
|
+
request = client(:reservation).build_request('OTA_HotelResRQ', attributes: attributes, message: message)
|
75
|
+
logger.info('BOOKING REQUEST') { request }
|
76
|
+
logger.info('BOOKING RESPONSE') { response.http.body }
|
77
|
+
end
|
78
|
+
|
79
|
+
response.body
|
80
|
+
rescue Savon::Error => e
|
81
|
+
Xlog.and_raise_error(e)
|
82
|
+
end
|
83
|
+
|
84
|
+
def cancel_request(type, parsec_id)
|
85
|
+
attributes = { xmlns: NAMESPACE, 'Transaction' => type }
|
86
|
+
message = { unique_i_d: { '@Type' => 'Locator', '@ID' => parsec_id } }
|
87
|
+
response = client(:cancel).call('OTA_CancelRQ', attributes: attributes, message: message)
|
88
|
+
if type == 'Cancel'
|
89
|
+
request = client(:reservation).build_request('OTA_HotelResRQ', attributes: attributes, message: message)
|
90
|
+
logger.info('CANCEL REQUEST') { request }
|
91
|
+
logger.info('CANCEL RESPONSE') { response.http.body }
|
92
|
+
end
|
93
|
+
|
94
|
+
response.body
|
95
|
+
rescue Savon::Error => e
|
96
|
+
Xlog.and_raise_error(e)
|
97
|
+
end
|
98
|
+
|
99
|
+
def rooms(booking_data)
|
100
|
+
booking_data.map { |data| { room_rate: { '@BookingCode' => data[:booking_code] } } }
|
101
|
+
end
|
102
|
+
|
103
|
+
def rooms_with_guests(booking_data, client_id)
|
104
|
+
rooms = booking_data.map do |data|
|
105
|
+
{
|
106
|
+
room_rate: { '@BookingCode' => data[:booking_code] },
|
107
|
+
guests: { guest: guests(data.slice(:adults, :children).merge(client_id: client_id)) }
|
108
|
+
}
|
109
|
+
end
|
110
|
+
rooms.first[:guests][:guest].first['@LeadGuest'] = 1
|
111
|
+
|
112
|
+
rooms
|
113
|
+
end
|
114
|
+
|
115
|
+
def guests(adults:, children: nil, client_id:)
|
116
|
+
pi_id = client_id.split('-').second
|
117
|
+
customer = ProductItem.find(pi_id).order.customer
|
118
|
+
guests = []
|
119
|
+
|
120
|
+
# rubocop:disable Metrics/LineLength
|
121
|
+
adults.to_i.times.each do
|
122
|
+
guests << { '@AgeCode' => 'A', person_name: { name_prefix: 'Mr.', given_name: customer.first_name, surname: customer.last_name } }
|
123
|
+
end
|
124
|
+
|
125
|
+
children.to_a.each do |age|
|
126
|
+
guests << { '@AgeCode' => 'C', '@Age' => age, person_name: { name_prefix: 'Mr.', given_name: customer.first_name, surname: customer.last_name } }
|
127
|
+
end
|
128
|
+
# rubocop:enable Metrics/LineLength
|
129
|
+
|
130
|
+
guests
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parsec
|
4
|
+
module Request
|
5
|
+
class Region < Base
|
6
|
+
def by_country(country_iso_code)
|
7
|
+
message = { read_request: { hotel_read_request: { request_type: 'GetRegions', country_code: country_iso_code } } }
|
8
|
+
response = client(:static_data).call('OTA_ReadRQ', message: message)
|
9
|
+
|
10
|
+
Array.wrap(response.body.dig(:ota_read_rs, :read_response, :regions, :region)).map do |r|
|
11
|
+
Parsec::Region.new(code: r[:@region_code], name: r[:region_name], country_iso: r[:country_iso])
|
12
|
+
end
|
13
|
+
rescue Savon::Error => e
|
14
|
+
Xlog.and_raise_error(e)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: parsec_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Kniazevych
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-01-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: savon
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/parsec.rb
|
34
|
+
- lib/parsec/availability.rb
|
35
|
+
- lib/parsec/base.rb
|
36
|
+
- lib/parsec/city.rb
|
37
|
+
- lib/parsec/configuration.rb
|
38
|
+
- lib/parsec/country.rb
|
39
|
+
- lib/parsec/hotel.rb
|
40
|
+
- lib/parsec/location.rb
|
41
|
+
- lib/parsec/order.rb
|
42
|
+
- lib/parsec/region.rb
|
43
|
+
- lib/parsec/request/availability.rb
|
44
|
+
- lib/parsec/request/base.rb
|
45
|
+
- lib/parsec/request/city.rb
|
46
|
+
- lib/parsec/request/country.rb
|
47
|
+
- lib/parsec/request/hotel.rb
|
48
|
+
- lib/parsec/request/location.rb
|
49
|
+
- lib/parsec/request/order.rb
|
50
|
+
- lib/parsec/request/region.rb
|
51
|
+
homepage:
|
52
|
+
licenses: []
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubygems_version: 3.0.8
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: parsec_lient cover most of functionality provided by Parsec API
|
73
|
+
test_files: []
|