parsec_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsec
4
+ class Base
5
+ DATE_FORMAT = '%Y-%m-%d'
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsec
4
+ class City < Base
5
+ attr_reader :code, :name, :country_iso
6
+
7
+ def initialize(code:, name: nil, country_iso: nil)
8
+ @code = code
9
+ @name = name
10
+ @country_iso = country_iso
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Parsec
2
+ class Configuration
3
+ attr_accessor :username, :password, :context, :host
4
+
5
+ def initialize
6
+ @username = nil
7
+ @password = nil
8
+ @context = nil
9
+ @host = nil
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsec
4
+ class Country < Base
5
+ attr_reader :code, :name, :iso
6
+
7
+ def initialize(iso:, name: nil, code: nil)
8
+ @code = code
9
+ @name = name
10
+ @iso = iso
11
+ end
12
+ end
13
+ end
@@ -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
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsec
4
+ class Region < Base
5
+ attr_reader :code, :name, :country_iso
6
+
7
+ def initialize(code:, name: nil, country_iso: nil)
8
+ @code = code
9
+ @name = name
10
+ @country_iso = country_iso
11
+ end
12
+ end
13
+ 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: []