parsec_client 0.0.1

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.
@@ -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: []