channex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +281 -0
- data/Rakefile +12 -0
- data/lib/channex/client.rb +283 -0
- data/lib/channex/configuration.rb +52 -0
- data/lib/channex/errors.rb +62 -0
- data/lib/channex/object.rb +201 -0
- data/lib/channex/objects/booking_revision.rb +124 -0
- data/lib/channex/objects/booking_room.rb +68 -0
- data/lib/channex/objects/company.rb +16 -0
- data/lib/channex/objects/customer.rb +43 -0
- data/lib/channex/objects/guarantee.rb +21 -0
- data/lib/channex/objects/guest.rb +21 -0
- data/lib/channex/objects/message.rb +95 -0
- data/lib/channex/objects/message_thread.rb +76 -0
- data/lib/channex/objects/occupancy.rb +29 -0
- data/lib/channex/objects/review.rb +87 -0
- data/lib/channex/objects/service.rb +44 -0
- data/lib/channex/objects/tax.rb +36 -0
- data/lib/channex/objects.rb +22 -0
- data/lib/channex/resource.rb +130 -0
- data/lib/channex/resources/attachments.rb +36 -0
- data/lib/channex/resources/availability.rb +87 -0
- data/lib/channex/resources/availability_rules.rb +52 -0
- data/lib/channex/resources/booking_revisions.rb +112 -0
- data/lib/channex/resources/bookings.rb +72 -0
- data/lib/channex/resources/channels.rb +96 -0
- data/lib/channex/resources/facilities.rb +29 -0
- data/lib/channex/resources/group_users.rb +76 -0
- data/lib/channex/resources/groups.rb +26 -0
- data/lib/channex/resources/hotel_policies.rb +54 -0
- data/lib/channex/resources/message_threads.rb +89 -0
- data/lib/channex/resources/messages.rb +64 -0
- data/lib/channex/resources/photos.rb +117 -0
- data/lib/channex/resources/properties.rb +42 -0
- data/lib/channex/resources/property_users.rb +32 -0
- data/lib/channex/resources/rate_plans.rb +34 -0
- data/lib/channex/resources/restrictions.rb +98 -0
- data/lib/channex/resources/reviews.rb +41 -0
- data/lib/channex/resources/room_types.rb +34 -0
- data/lib/channex/resources/tax_sets.rb +34 -0
- data/lib/channex/resources/taxes.rb +60 -0
- data/lib/channex/resources/webhooks.rb +87 -0
- data/lib/channex/resources.rb +5 -0
- data/lib/channex/response.rb +102 -0
- data/lib/channex/version.rb +3 -0
- data/lib/channex.rb +52 -0
- data/sig/channex.rbs +4 -0
- metadata +124 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Photos API resource
|
|
4
|
+
#
|
|
5
|
+
# Photos are associated with Properties and optionally with Room Types.
|
|
6
|
+
# Position 0 is used as the cover photo.
|
|
7
|
+
#
|
|
8
|
+
# @example List photos for a property
|
|
9
|
+
# client.photos.list(property_id: "property-uuid")
|
|
10
|
+
#
|
|
11
|
+
# @example Create a photo from URL
|
|
12
|
+
# client.photos.create(
|
|
13
|
+
# property_id: "property-uuid",
|
|
14
|
+
# url: "https://example.com/photo.jpg",
|
|
15
|
+
# position: 0,
|
|
16
|
+
# description: "Hotel Lobby",
|
|
17
|
+
# kind: "photo"
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @example Upload a photo file then create it
|
|
21
|
+
# # Step 1: Upload the file to get a temporary URL
|
|
22
|
+
# upload_response = client.photos.upload("/path/to/photo.jpg")
|
|
23
|
+
# temp_url = upload_response.body["url"]
|
|
24
|
+
#
|
|
25
|
+
# # Step 2: Create the photo using the temporary URL
|
|
26
|
+
# client.photos.create(
|
|
27
|
+
# property_id: "property-uuid",
|
|
28
|
+
# url: temp_url,
|
|
29
|
+
# position: 0
|
|
30
|
+
# )
|
|
31
|
+
#
|
|
32
|
+
# @see https://docs.channex.io/api-v.1-documentation/photos-collection
|
|
33
|
+
#
|
|
34
|
+
class Photos < Resource
|
|
35
|
+
KINDS = %w[photo ad menu].freeze
|
|
36
|
+
|
|
37
|
+
# Upload a photo file and get a temporary URL
|
|
38
|
+
#
|
|
39
|
+
# This uploads the file to Channex's servers and returns a temporary URL
|
|
40
|
+
# that can then be used with the create method.
|
|
41
|
+
#
|
|
42
|
+
# @param file_path [String] Path to the photo file
|
|
43
|
+
# @return [Response] Response with "url" in body containing temporary URL
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# response = client.photos.upload("/path/to/photo.jpg")
|
|
47
|
+
# temp_url = response.body["url"]
|
|
48
|
+
# # => "https://ams3.digitaloceanspaces.com/temporaryphotos/..."
|
|
49
|
+
def upload(file_path)
|
|
50
|
+
uri = URI("#{client.configuration.api_url}#{resource_path}/upload")
|
|
51
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
52
|
+
http.use_ssl = uri.scheme == "https"
|
|
53
|
+
http.read_timeout = client.configuration.timeout
|
|
54
|
+
http.open_timeout = client.configuration.open_timeout
|
|
55
|
+
|
|
56
|
+
request = Net::HTTP::Post.new(uri)
|
|
57
|
+
request["user-api-key"] = client.api_key
|
|
58
|
+
|
|
59
|
+
file = File.open(file_path, "rb")
|
|
60
|
+
filename = File.basename(file_path)
|
|
61
|
+
boundary = "----RubyMultipartPost#{rand(1000000)}"
|
|
62
|
+
|
|
63
|
+
request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
|
64
|
+
request.body = build_multipart_body(file, filename, boundary)
|
|
65
|
+
file.close
|
|
66
|
+
|
|
67
|
+
response = http.request(request)
|
|
68
|
+
Response.new(response)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Reorder photos
|
|
72
|
+
#
|
|
73
|
+
# @param photos [Array<Hash>] Array of { id: "uuid", position: 0 }
|
|
74
|
+
# @return [Response]
|
|
75
|
+
def reorder(photos)
|
|
76
|
+
client.post("#{resource_path}/reorder", body: {photos: photos})
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
protected
|
|
80
|
+
|
|
81
|
+
def resource_path
|
|
82
|
+
"/photos"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def filter_keys
|
|
86
|
+
%i[property_id room_type_id]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def build_multipart_body(file, filename, boundary)
|
|
92
|
+
body = []
|
|
93
|
+
body << "--#{boundary}\r\n"
|
|
94
|
+
body << "Content-Disposition: form-data; name=\"photo\"; filename=\"#{filename}\"\r\n"
|
|
95
|
+
body << "Content-Type: #{mime_type_for(filename)}\r\n\r\n"
|
|
96
|
+
body << file.read
|
|
97
|
+
body << "\r\n--#{boundary}--\r\n"
|
|
98
|
+
body.join
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def mime_type_for(filename)
|
|
102
|
+
case File.extname(filename).downcase
|
|
103
|
+
when ".jpg", ".jpeg"
|
|
104
|
+
"image/jpeg"
|
|
105
|
+
when ".png"
|
|
106
|
+
"image/png"
|
|
107
|
+
when ".gif"
|
|
108
|
+
"image/gif"
|
|
109
|
+
when ".webp"
|
|
110
|
+
"image/webp"
|
|
111
|
+
else
|
|
112
|
+
"application/octet-stream"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Properties API resource
|
|
4
|
+
#
|
|
5
|
+
# A Property represents a physical premises - hotels, motels, lodges,
|
|
6
|
+
# cabins, chalets, luxury apartments, etc.
|
|
7
|
+
#
|
|
8
|
+
# @example List all properties
|
|
9
|
+
# client.properties.list
|
|
10
|
+
#
|
|
11
|
+
# @example Create a property
|
|
12
|
+
# client.properties.create(
|
|
13
|
+
# title: "My Hotel",
|
|
14
|
+
# currency: "USD",
|
|
15
|
+
# timezone: "America/New_York",
|
|
16
|
+
# property_type: "hotel",
|
|
17
|
+
# settings: { allow_availability_autoupdate: true }
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @see https://docs.channex.io/api-v.1-documentation/hotels-collection
|
|
21
|
+
#
|
|
22
|
+
class Properties < Resource
|
|
23
|
+
# Retrieve property health status
|
|
24
|
+
#
|
|
25
|
+
# @param id [String] Property UUID
|
|
26
|
+
# @return [Response]
|
|
27
|
+
def health(id)
|
|
28
|
+
client.get("#{resource_path}/#{id}/health")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
protected
|
|
32
|
+
|
|
33
|
+
def resource_path
|
|
34
|
+
"/properties"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def filter_keys
|
|
38
|
+
%i[group_id]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Property Users API resource
|
|
4
|
+
#
|
|
5
|
+
# Property Users represent the association between users and properties,
|
|
6
|
+
# defining access permissions at the property level.
|
|
7
|
+
#
|
|
8
|
+
# @example List users for a property
|
|
9
|
+
# client.property_users.list(property_id: "property-uuid")
|
|
10
|
+
#
|
|
11
|
+
# @example Add a user to a property
|
|
12
|
+
# client.property_users.create(
|
|
13
|
+
# property_id: "property-uuid",
|
|
14
|
+
# user_id: "user-uuid",
|
|
15
|
+
# role: "manager"
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# @see https://docs.channex.io/api-v.1-documentation/property-users-collection
|
|
19
|
+
#
|
|
20
|
+
class PropertyUsers < Resource
|
|
21
|
+
protected
|
|
22
|
+
|
|
23
|
+
def resource_path
|
|
24
|
+
"/property_users"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filter_keys
|
|
28
|
+
%i[property_id]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Rate Plans API resource
|
|
4
|
+
#
|
|
5
|
+
# A Rate Plan is a pricing plan for how to sell your Room Types,
|
|
6
|
+
# containing information about prices and restrictions.
|
|
7
|
+
#
|
|
8
|
+
# @example List rate plans for a property
|
|
9
|
+
# client.rate_plans.list(property_id: "property-uuid")
|
|
10
|
+
#
|
|
11
|
+
# @example Create a rate plan
|
|
12
|
+
# client.rate_plans.create(
|
|
13
|
+
# property_id: "property-uuid",
|
|
14
|
+
# room_type_id: "room-type-uuid",
|
|
15
|
+
# title: "Standard Rate",
|
|
16
|
+
# sell_mode: "per_room",
|
|
17
|
+
# rate_mode: "manual"
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @see https://docs.channex.io/api-v.1-documentation/rate-plans-collection
|
|
21
|
+
#
|
|
22
|
+
class RatePlans < Resource
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def resource_path
|
|
26
|
+
"/rate_plans"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def filter_keys
|
|
30
|
+
%i[property_id room_type_id]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Restrictions API resource
|
|
4
|
+
#
|
|
5
|
+
# Restrictions represent pricing and stay restrictions applied to rate plans.
|
|
6
|
+
# This includes rates, min/max stay, closed to arrival/departure, and stop sell.
|
|
7
|
+
#
|
|
8
|
+
# @example Get restrictions for a property
|
|
9
|
+
# client.restrictions.list(
|
|
10
|
+
# property_id: "property-uuid",
|
|
11
|
+
# date_from: "2024-01-01",
|
|
12
|
+
# date_to: "2024-01-31"
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# @example Update restrictions (rates and stay rules)
|
|
16
|
+
# client.restrictions.update([
|
|
17
|
+
# {
|
|
18
|
+
# property_id: "property-uuid",
|
|
19
|
+
# rate_plan_id: "rate-plan-uuid",
|
|
20
|
+
# date_from: "2024-01-15",
|
|
21
|
+
# date_to: "2024-01-20",
|
|
22
|
+
# rate: 15000,
|
|
23
|
+
# min_stay_arrival: 2,
|
|
24
|
+
# closed_to_arrival: false
|
|
25
|
+
# }
|
|
26
|
+
# ])
|
|
27
|
+
#
|
|
28
|
+
# @see https://docs.channex.io/api-v.1-documentation/ari
|
|
29
|
+
#
|
|
30
|
+
class Restrictions < Resource
|
|
31
|
+
# Get restrictions for a property within a date range
|
|
32
|
+
#
|
|
33
|
+
# @param property_id [String] Property UUID (required)
|
|
34
|
+
# @param date [String] Specific date (YYYY-MM-DD)
|
|
35
|
+
# @param date_from [String] Start date (YYYY-MM-DD)
|
|
36
|
+
# @param date_to [String] End date (YYYY-MM-DD)
|
|
37
|
+
# @param options [Hash] Additional options
|
|
38
|
+
# @return [Response]
|
|
39
|
+
def list(property_id:, date: nil, date_from: nil, date_to: nil, **options)
|
|
40
|
+
filter = options.delete(:filter) || {}
|
|
41
|
+
filter[:property_id] = property_id
|
|
42
|
+
|
|
43
|
+
if date
|
|
44
|
+
filter[:date] = date
|
|
45
|
+
else
|
|
46
|
+
filter[:date] = {}
|
|
47
|
+
filter[:date][:gte] = date_from if date_from
|
|
48
|
+
filter[:date][:lte] = date_to if date_to
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
client.get(resource_path, params: {filter: filter, **options})
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Update restrictions (rates and stay rules) for rate plans
|
|
55
|
+
#
|
|
56
|
+
# Each value in the array should contain all required fields including property_id.
|
|
57
|
+
# You can update multiple properties and rate plans in a single request.
|
|
58
|
+
#
|
|
59
|
+
# @param values [Array<Hash>] Array of restriction updates
|
|
60
|
+
# Each hash can contain:
|
|
61
|
+
# - property_id [String] Property UUID (required)
|
|
62
|
+
# - rate_plan_id [String] Rate Plan UUID (required)
|
|
63
|
+
# - date_from [String] Start date YYYY-MM-DD (required)
|
|
64
|
+
# - date_to [String] End date YYYY-MM-DD (required)
|
|
65
|
+
# - rate [Integer] Price (in smallest currency unit, e.g., cents)
|
|
66
|
+
# - min_stay_arrival [Integer] Minimum stay if arriving on this date
|
|
67
|
+
# - min_stay_through [Integer] Minimum stay passing through this date
|
|
68
|
+
# - max_stay [Integer] Maximum stay (0 = no limit)
|
|
69
|
+
# - closed_to_arrival [Boolean] Prevent arrivals on this date
|
|
70
|
+
# - closed_to_departure [Boolean] Prevent departures on this date
|
|
71
|
+
# - stop_sell [Boolean] Stop selling this rate plan
|
|
72
|
+
# @return [Response]
|
|
73
|
+
#
|
|
74
|
+
# @example Update rates and restrictions for a date range
|
|
75
|
+
# client.restrictions.update([
|
|
76
|
+
# {
|
|
77
|
+
# property_id: "716305c4-561a-4561-a187-7f5b8aeb5920",
|
|
78
|
+
# rate_plan_id: "bab451e7-9ab1-4cc4-aa16-107bf7bbabb2",
|
|
79
|
+
# date_from: "2024-01-15",
|
|
80
|
+
# date_to: "2024-01-20",
|
|
81
|
+
# rate: 30000,
|
|
82
|
+
# min_stay_arrival: 2,
|
|
83
|
+
# closed_to_arrival: false,
|
|
84
|
+
# stop_sell: false
|
|
85
|
+
# }
|
|
86
|
+
# ])
|
|
87
|
+
def update(values)
|
|
88
|
+
client.post(resource_path, body: {values: values})
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
protected
|
|
92
|
+
|
|
93
|
+
def resource_path
|
|
94
|
+
"/restrictions"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Reviews API resource
|
|
4
|
+
#
|
|
5
|
+
# A review is what a guest wrote about a stay on Booking.com, Airbnb or
|
|
6
|
+
# Expedia, normalised by Channex: an overall score out of 10, per-category
|
|
7
|
+
# scores, the text and the property's reply.
|
|
8
|
+
#
|
|
9
|
+
# Requires the Messages & Reviews application to be installed on the
|
|
10
|
+
# property — without it every call in here answers 403 Forbidden.
|
|
11
|
+
#
|
|
12
|
+
# @example List a property's reviews
|
|
13
|
+
# client.reviews.list(property_id: "property-uuid")
|
|
14
|
+
#
|
|
15
|
+
# @example Reply to a review
|
|
16
|
+
# client.reviews.reply("review-uuid", reply: "Thank you for staying with us.")
|
|
17
|
+
#
|
|
18
|
+
# @see https://docs.channex.io/api-v.1-documentation/reviews-collection
|
|
19
|
+
#
|
|
20
|
+
class Reviews < Resource
|
|
21
|
+
# Reply to a review on the OTA
|
|
22
|
+
#
|
|
23
|
+
# @param id [String] Review UUID
|
|
24
|
+
# @param reply [String] Reply text
|
|
25
|
+
# @return [Response] The review, now replied
|
|
26
|
+
def reply(id, reply:)
|
|
27
|
+
client.post("#{resource_path}/#{id}/reply", body: {reply: {reply:}})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def resource_path
|
|
33
|
+
"/reviews"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def filter_keys
|
|
37
|
+
%i[property_id booking_id]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Room Types API resource
|
|
4
|
+
#
|
|
5
|
+
# Room Types represent the different types of rooms available at a property.
|
|
6
|
+
#
|
|
7
|
+
# @example List room types for a property
|
|
8
|
+
# client.room_types.list(property_id: "property-uuid")
|
|
9
|
+
#
|
|
10
|
+
# @example Create a room type
|
|
11
|
+
# client.room_types.create(
|
|
12
|
+
# property_id: "property-uuid",
|
|
13
|
+
# title: "Deluxe King Room",
|
|
14
|
+
# count_of_rooms: 10,
|
|
15
|
+
# occ_adults: 2,
|
|
16
|
+
# occ_children: 1,
|
|
17
|
+
# occ_infants: 1
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @see https://docs.channex.io/api-v.1-documentation/room-types-collection
|
|
21
|
+
#
|
|
22
|
+
class RoomTypes < Resource
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def resource_path
|
|
26
|
+
"/room_types"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def filter_keys
|
|
30
|
+
%i[property_id]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Tax Sets API resource
|
|
4
|
+
#
|
|
5
|
+
# A Tax Set represents a group of Taxes applicable to your Rate Plan or Property.
|
|
6
|
+
# Each property can have many Tax Sets, but only one can be selected as
|
|
7
|
+
# the Default Tax Set, which is applied to each new Rate Plan automatically.
|
|
8
|
+
#
|
|
9
|
+
# @example List tax sets for a property
|
|
10
|
+
# client.tax_sets.list(property_id: "property-uuid")
|
|
11
|
+
#
|
|
12
|
+
# @example Create a tax set
|
|
13
|
+
# client.tax_sets.create(
|
|
14
|
+
# property_id: "property-uuid",
|
|
15
|
+
# title: "Standard Taxes",
|
|
16
|
+
# taxes: ["tax-uuid-1", "tax-uuid-2"],
|
|
17
|
+
# is_default: true
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @see https://docs.channex.io/api-v.1-documentation/taxes-and-tax-sets
|
|
21
|
+
#
|
|
22
|
+
class TaxSets < Resource
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def resource_path
|
|
26
|
+
"/tax_sets"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def filter_keys
|
|
30
|
+
%i[property_id]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Taxes API resource
|
|
4
|
+
#
|
|
5
|
+
# A Tax represents a specific tax applicable to your Rate Plan or Property,
|
|
6
|
+
# such as VAT 20%, City Tax 1 EUR per Guest per Night, etc.
|
|
7
|
+
#
|
|
8
|
+
# @example List taxes for a property
|
|
9
|
+
# client.taxes.list(property_id: "property-uuid")
|
|
10
|
+
#
|
|
11
|
+
# @example Create a percentage-based tax (VAT)
|
|
12
|
+
# client.taxes.create(
|
|
13
|
+
# property_id: "property-uuid",
|
|
14
|
+
# title: "VAT",
|
|
15
|
+
# logic: "percent",
|
|
16
|
+
# type: "tax",
|
|
17
|
+
# rate: "20.00",
|
|
18
|
+
# is_inclusive: true
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Create a fixed amount tax (City Tax per person per night)
|
|
22
|
+
# client.taxes.create(
|
|
23
|
+
# property_id: "property-uuid",
|
|
24
|
+
# title: "City Tax",
|
|
25
|
+
# logic: "per_person_per_night",
|
|
26
|
+
# type: "city tax",
|
|
27
|
+
# rate: "2.00",
|
|
28
|
+
# currency: "EUR",
|
|
29
|
+
# is_inclusive: false
|
|
30
|
+
# )
|
|
31
|
+
#
|
|
32
|
+
# @see https://docs.channex.io/api-v.1-documentation/taxes-and-tax-sets
|
|
33
|
+
#
|
|
34
|
+
class Taxes < Resource
|
|
35
|
+
# Logic types for tax calculation
|
|
36
|
+
LOGIC_TYPES = %w[
|
|
37
|
+
percent
|
|
38
|
+
per_room
|
|
39
|
+
per_room_per_night
|
|
40
|
+
per_person
|
|
41
|
+
per_person_per_night
|
|
42
|
+
per_night
|
|
43
|
+
per_booking
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
# Tax types
|
|
47
|
+
TAX_TYPES = ["tax", "fee", "city tax"].freeze
|
|
48
|
+
|
|
49
|
+
protected
|
|
50
|
+
|
|
51
|
+
def resource_path
|
|
52
|
+
"/taxes"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def filter_keys
|
|
56
|
+
%i[property_id]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
module Resources
|
|
3
|
+
# Webhooks API resource
|
|
4
|
+
#
|
|
5
|
+
# Webhooks allow you to receive push notifications about changes
|
|
6
|
+
# in your property's data, including ARI changes and booking events.
|
|
7
|
+
#
|
|
8
|
+
# Available webhook events:
|
|
9
|
+
# - *: All events
|
|
10
|
+
# - ari: ARI changes (availability, rates, restrictions)
|
|
11
|
+
# - booking: All booking changes (new, modified, cancelled)
|
|
12
|
+
# - booking_new: Only new bookings
|
|
13
|
+
# - booking_modification: Only modified bookings
|
|
14
|
+
# - booking_cancellation: Only cancelled bookings
|
|
15
|
+
# - reservation_request: Airbnb reservation requests
|
|
16
|
+
# - new_channel: Channel connected
|
|
17
|
+
# - updated_channel: Channel updated
|
|
18
|
+
# - disconnected_channel: Channel disconnected
|
|
19
|
+
# - message: New message received
|
|
20
|
+
#
|
|
21
|
+
# ARI events support filtering by restriction type, room type, and rate plan:
|
|
22
|
+
# Format: "ari:restrictions:room_type_ids:rate_plan_ids"
|
|
23
|
+
# Example: "ari:rate:*:96a44e07-2158-43e4-8baa-8f6f56922ba8"
|
|
24
|
+
#
|
|
25
|
+
# @example Create a webhook for all events
|
|
26
|
+
# client.webhooks.create(
|
|
27
|
+
# property_id: "property-uuid",
|
|
28
|
+
# callback_url: "https://your-domain.com/webhooks/channex",
|
|
29
|
+
# event_mask: "*",
|
|
30
|
+
# is_active: true,
|
|
31
|
+
# send_data: true
|
|
32
|
+
# )
|
|
33
|
+
#
|
|
34
|
+
# @example Create a webhook for specific rate plan changes
|
|
35
|
+
# client.webhooks.create(
|
|
36
|
+
# property_id: "property-uuid",
|
|
37
|
+
# callback_url: "https://your-domain.com/webhooks/ari",
|
|
38
|
+
# event_mask: "ari:rate:*:96a44e07-2158-43e4-8baa-8f6f56922ba8",
|
|
39
|
+
# is_active: true,
|
|
40
|
+
# send_data: true
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
# @see https://docs.channex.io/api-v.1-documentation/webhook-collection
|
|
44
|
+
#
|
|
45
|
+
class Webhooks < Resource
|
|
46
|
+
# Standard event types
|
|
47
|
+
EVENTS = %w[
|
|
48
|
+
ari
|
|
49
|
+
booking
|
|
50
|
+
booking_new
|
|
51
|
+
booking_modification
|
|
52
|
+
booking_cancellation
|
|
53
|
+
reservation_request
|
|
54
|
+
new_channel
|
|
55
|
+
updated_channel
|
|
56
|
+
disconnected_channel
|
|
57
|
+
message
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
# Test a webhook endpoint without saving
|
|
61
|
+
#
|
|
62
|
+
# @param attributes [Hash] Webhook attributes to test
|
|
63
|
+
# @return [Response]
|
|
64
|
+
def test_new(attributes)
|
|
65
|
+
client.post("#{resource_path}/test", body: {webhook: attributes})
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Test an existing webhook endpoint
|
|
69
|
+
#
|
|
70
|
+
# @param id [String] Webhook UUID
|
|
71
|
+
# @return [Response]
|
|
72
|
+
def test(id)
|
|
73
|
+
client.post("#{resource_path}/#{id}/test", body: {})
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
protected
|
|
77
|
+
|
|
78
|
+
def resource_path
|
|
79
|
+
"/webhooks"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def filter_keys
|
|
83
|
+
%i[property_id]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module Channex
|
|
2
|
+
# Wraps API responses with convenient accessors
|
|
3
|
+
#
|
|
4
|
+
# Channex API responses follow this structure:
|
|
5
|
+
# - Success: { "data": {...} } or { "data": [...], "meta": {...} }
|
|
6
|
+
# - Error: { "error": {...} } or { "errors": {...} }
|
|
7
|
+
#
|
|
8
|
+
class Response
|
|
9
|
+
attr_reader :status, :headers, :raw_body, :body
|
|
10
|
+
|
|
11
|
+
def initialize(http_response)
|
|
12
|
+
@status = http_response.code.to_i
|
|
13
|
+
@headers = http_response.to_hash
|
|
14
|
+
@raw_body = http_response.body
|
|
15
|
+
@body = parse_body(http_response.body)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def success?
|
|
19
|
+
status >= 200 && status < 300
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def data
|
|
23
|
+
body["data"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def meta
|
|
27
|
+
body["meta"] || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns error information from the response
|
|
31
|
+
#
|
|
32
|
+
# Channex API can return errors in two formats:
|
|
33
|
+
# - { "error": { "message": "...", "code": "..." } }
|
|
34
|
+
# - { "errors": { "code": "...", "title": "..." } }
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash, nil] Error information or nil if no error
|
|
37
|
+
def error
|
|
38
|
+
body["error"] || body["errors"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns warnings from the response meta
|
|
42
|
+
#
|
|
43
|
+
# ARI updates can return partial success with warnings for invalid values.
|
|
44
|
+
#
|
|
45
|
+
# @return [Array] Array of warning objects
|
|
46
|
+
def warnings
|
|
47
|
+
meta["warnings"] || []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pagination
|
|
51
|
+
@pagination ||= Pagination.new(meta)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def parse_body(body)
|
|
57
|
+
return {} if body.nil? || body.empty?
|
|
58
|
+
|
|
59
|
+
JSON.parse(body)
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
{"raw" => body}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Pagination information from API responses
|
|
66
|
+
class Pagination
|
|
67
|
+
attr_reader :page, :limit, :total, :order
|
|
68
|
+
|
|
69
|
+
def initialize(meta)
|
|
70
|
+
@page = meta["page"]
|
|
71
|
+
@limit = meta["limit"]
|
|
72
|
+
@total = meta["total"]
|
|
73
|
+
@order = meta["order"]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def total_pages
|
|
77
|
+
return nil unless total && limit && limit > 0
|
|
78
|
+
|
|
79
|
+
(total.to_f / limit).ceil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def next_page
|
|
83
|
+
return nil unless page && total_pages
|
|
84
|
+
|
|
85
|
+
(page < total_pages) ? page + 1 : nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def prev_page
|
|
89
|
+
return nil unless page
|
|
90
|
+
|
|
91
|
+
(page > 1) ? page - 1 : nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def has_next?
|
|
95
|
+
!next_page.nil?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def has_prev?
|
|
99
|
+
!prev_page.nil?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|