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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE +21 -0
  4. data/README.md +281 -0
  5. data/Rakefile +12 -0
  6. data/lib/channex/client.rb +283 -0
  7. data/lib/channex/configuration.rb +52 -0
  8. data/lib/channex/errors.rb +62 -0
  9. data/lib/channex/object.rb +201 -0
  10. data/lib/channex/objects/booking_revision.rb +124 -0
  11. data/lib/channex/objects/booking_room.rb +68 -0
  12. data/lib/channex/objects/company.rb +16 -0
  13. data/lib/channex/objects/customer.rb +43 -0
  14. data/lib/channex/objects/guarantee.rb +21 -0
  15. data/lib/channex/objects/guest.rb +21 -0
  16. data/lib/channex/objects/message.rb +95 -0
  17. data/lib/channex/objects/message_thread.rb +76 -0
  18. data/lib/channex/objects/occupancy.rb +29 -0
  19. data/lib/channex/objects/review.rb +87 -0
  20. data/lib/channex/objects/service.rb +44 -0
  21. data/lib/channex/objects/tax.rb +36 -0
  22. data/lib/channex/objects.rb +22 -0
  23. data/lib/channex/resource.rb +130 -0
  24. data/lib/channex/resources/attachments.rb +36 -0
  25. data/lib/channex/resources/availability.rb +87 -0
  26. data/lib/channex/resources/availability_rules.rb +52 -0
  27. data/lib/channex/resources/booking_revisions.rb +112 -0
  28. data/lib/channex/resources/bookings.rb +72 -0
  29. data/lib/channex/resources/channels.rb +96 -0
  30. data/lib/channex/resources/facilities.rb +29 -0
  31. data/lib/channex/resources/group_users.rb +76 -0
  32. data/lib/channex/resources/groups.rb +26 -0
  33. data/lib/channex/resources/hotel_policies.rb +54 -0
  34. data/lib/channex/resources/message_threads.rb +89 -0
  35. data/lib/channex/resources/messages.rb +64 -0
  36. data/lib/channex/resources/photos.rb +117 -0
  37. data/lib/channex/resources/properties.rb +42 -0
  38. data/lib/channex/resources/property_users.rb +32 -0
  39. data/lib/channex/resources/rate_plans.rb +34 -0
  40. data/lib/channex/resources/restrictions.rb +98 -0
  41. data/lib/channex/resources/reviews.rb +41 -0
  42. data/lib/channex/resources/room_types.rb +34 -0
  43. data/lib/channex/resources/tax_sets.rb +34 -0
  44. data/lib/channex/resources/taxes.rb +60 -0
  45. data/lib/channex/resources/webhooks.rb +87 -0
  46. data/lib/channex/resources.rb +5 -0
  47. data/lib/channex/response.rb +102 -0
  48. data/lib/channex/version.rb +3 -0
  49. data/lib/channex.rb +52 -0
  50. data/sig/channex.rbs +4 -0
  51. metadata +124 -0
@@ -0,0 +1,112 @@
1
+ module Channex
2
+ module Resources
3
+ # Booking Revisions API resource
4
+ #
5
+ # Booking Revisions are snapshots of bookings at a given time.
6
+ # Each time an OTA provides an update, a new revision is created.
7
+ # The feed endpoint is the primary way to receive new bookings from Channex.
8
+ #
9
+ # IMPORTANT: You must acknowledge bookings after processing them.
10
+ # Not acknowledging bookings will result in them being repeatedly
11
+ # returned for 30 minutes, followed by email warnings.
12
+ #
13
+ # @example Get unacknowledged booking revisions (typed objects)
14
+ # response = client.booking_revisions.feed
15
+ # response.objects.each do |revision|
16
+ # puts revision.customer.full_name
17
+ # revision.rooms.each { |room| puts room.nights }
18
+ # client.booking_revisions.acknowledge(revision.id)
19
+ # end
20
+ #
21
+ # @example Hash access still works for backwards compatibility
22
+ # response = client.booking_revisions.feed
23
+ # response.data.each do |revision|
24
+ # puts revision["attributes"]["customer"]["name"]
25
+ # end
26
+ #
27
+ # @see https://docs.channex.io/api-v.1-documentation/bookings-collection
28
+ #
29
+ class BookingRevisions < Resource
30
+ # Get unacknowledged booking revisions from the feed
31
+ #
32
+ # Returns a response with both raw data and typed objects.
33
+ # Use response.objects for typed access, response.data for raw hashes.
34
+ #
35
+ # @param order [Hash] Ordering options (e.g., { inserted_at: :asc })
36
+ # @return [Response] Response with #objects method for typed access
37
+ def feed(order: {inserted_at: :asc})
38
+ response = client.get("#{resource_path}/feed", params: {order: order})
39
+ add_objects_wrapper(response)
40
+ response
41
+ end
42
+
43
+ # Acknowledge a booking revision
44
+ #
45
+ # Once acknowledged, the revision will not appear in the feed again.
46
+ #
47
+ # @param id [String] Booking revision UUID
48
+ # @return [Response]
49
+ def acknowledge(id)
50
+ client.post("#{resource_path}/#{id}/ack", body: {})
51
+ end
52
+
53
+ # Acknowledge multiple booking revisions at once
54
+ #
55
+ # @param ids [Array<String>] Array of booking revision UUIDs
56
+ # @return [Array<Response>]
57
+ def acknowledge_all(ids)
58
+ ids.map { |id| acknowledge(id) }
59
+ end
60
+
61
+ # Retrieve a single booking revision
62
+ #
63
+ # Returns a response with both raw data and a typed object.
64
+ # Use response.object for typed access, response.data for raw hash.
65
+ #
66
+ # @param id [String] Booking revision UUID
67
+ # @return [Response] Response with #object method for typed access
68
+ def retrieve(id)
69
+ response = super
70
+ add_object_wrapper(response)
71
+ response
72
+ end
73
+
74
+ # List booking revisions, filtered by booking_id
75
+ #
76
+ # @return [Response] Response with #objects method for typed access
77
+ def list(**)
78
+ response = super
79
+ add_objects_wrapper(response)
80
+ response
81
+ end
82
+
83
+ protected
84
+
85
+ def resource_path
86
+ "/booking_revisions"
87
+ end
88
+
89
+ def filter_keys
90
+ %i[booking_id]
91
+ end
92
+
93
+ private
94
+
95
+ # Add #object method to response for single revision
96
+ def add_object_wrapper(response)
97
+ response.define_singleton_method(:object) do
98
+ @_object ||= Channex::Objects::BookingRevision.construct_from(data&.dig("attributes"))
99
+ end
100
+ end
101
+
102
+ # Add #objects method to response for multiple revisions
103
+ def add_objects_wrapper(response)
104
+ response.define_singleton_method(:objects) do
105
+ @_objects ||= (data || []).map do |item|
106
+ Channex::Objects::BookingRevision.construct_from(item["attributes"])
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,72 @@
1
+ module Channex
2
+ module Resources
3
+ # Bookings API resource
4
+ #
5
+ # Bookings represent reservations made through channels connected to Channex.
6
+ # For receiving new bookings, use the BookingRevisions resource with the feed endpoint.
7
+ #
8
+ # @example List bookings
9
+ # client.bookings.list(property_id: "property-uuid")
10
+ #
11
+ # @example Retrieve a booking
12
+ # client.bookings.retrieve("booking-uuid")
13
+ #
14
+ # @example Create a booking (requires Booking CRS App)
15
+ # client.bookings.create(
16
+ # property_id: "property-uuid",
17
+ # rooms: [{
18
+ # room_type_id: "room-type-uuid",
19
+ # rate_plan_id: "rate-plan-uuid",
20
+ # checkin_date: "2024-02-01",
21
+ # checkout_date: "2024-02-05",
22
+ # guests: [{ name: "John Doe" }]
23
+ # }]
24
+ # )
25
+ #
26
+ # @see https://docs.channex.io/api-v.1-documentation/bookings-collection
27
+ # @see https://docs.channex.io/api-v.1-documentation/booking-crs-api
28
+ #
29
+ class Bookings < Resource
30
+ # List bookings; date_from and date_to bound the arrival date
31
+ #
32
+ # @return [Response]
33
+ def list(date_from: nil, date_to: nil, filter: {}, **options)
34
+ arrival_date = {gte: date_from, lte: date_to}.compact
35
+ filter = filter.merge(arrival_date:) if arrival_date.any?
36
+ super(filter:, **options)
37
+ end
38
+
39
+ # Revise (modify) an existing booking
40
+ #
41
+ # @param reservation_id [String] The reservation ID (part after "-" in unique_id)
42
+ # @param attributes [Hash] Updated booking attributes with status: "modified"
43
+ # @return [Response]
44
+ def revise(reservation_id, attributes)
45
+ client.post(
46
+ "#{resource_path}/revise",
47
+ body: {
48
+ booking: attributes.merge(reservation_id: reservation_id, status: "modified")
49
+ }
50
+ )
51
+ end
52
+
53
+ # Cancel a booking, including one that came from an OTA (requires Booking CRS App)
54
+ #
55
+ # @param booking_id [String] The booking UUID
56
+ # @return [Response]
57
+ def cancel(booking_id)
58
+ update(booking_id, status: "cancelled")
59
+ end
60
+
61
+ protected
62
+
63
+ def resource_path
64
+ "/bookings"
65
+ end
66
+
67
+ def filter_keys
68
+ %i[property_id channel_id status]
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,96 @@
1
+ module Channex
2
+ module Resources
3
+ # Channels API resource
4
+ #
5
+ # Channels represent connections to OTAs (Online Travel Agencies) like
6
+ # Booking.com, Airbnb, Expedia, etc.
7
+ #
8
+ # @example List channels for a property
9
+ # client.channels.list(property_id: "property-uuid")
10
+ #
11
+ # @example Retrieve a channel
12
+ # client.channels.retrieve("channel-uuid")
13
+ #
14
+ # @see https://docs.channex.io/api-v.1-documentation/channel-api
15
+ #
16
+ class Channels < Resource
17
+ # Get list of available channel codes/types
18
+ #
19
+ # @return [Response]
20
+ def codes
21
+ client.get("/channel_codes")
22
+ end
23
+
24
+ # Test OTA credentials before connecting. Returns the OTA's acknowledgement
25
+ # that the hotel_id + channel are reachable. The first call also reveals
26
+ # whether the account has Channel API access.
27
+ #
28
+ # @param channel [String] OTA code (e.g. "BookingCom")
29
+ # @param settings [Hash] OTA settings, at least { hotel_id: "..." }
30
+ # @return [Response]
31
+ def test_connection(channel:, settings:)
32
+ client.post("#{resource_path}/test_connection", body: {channel:, settings:})
33
+ end
34
+
35
+ # Fetch the OTA's rooms and rates to map against local rate plans. For
36
+ # Booking.com this returns rooms[].rates[], each keyed by an integer code.
37
+ #
38
+ # @param channel [String] OTA code (e.g. "BookingCom")
39
+ # @param settings [Hash] OTA settings, at least { hotel_id: "..." }
40
+ # @return [Response]
41
+ def mapping_details(channel:, settings:)
42
+ client.post("#{resource_path}/mapping_details", body: {channel:, settings:})
43
+ end
44
+
45
+ # Connect a new channel to a property
46
+ #
47
+ # @param attributes [Hash] Channel attributes including property_id, channel code, credentials
48
+ # @return [Response]
49
+ def connect(attributes)
50
+ create(attributes)
51
+ end
52
+
53
+ # Channels are created inactive; activate to go live.
54
+ #
55
+ # @param id [String] Channel UUID
56
+ # @return [Response]
57
+ def activate(id)
58
+ client.post("#{resource_path}/#{id}/activate", body: {})
59
+ end
60
+
61
+ # Pause a live channel without deleting it.
62
+ #
63
+ # @param id [String] Channel UUID
64
+ # @return [Response]
65
+ def deactivate(id)
66
+ client.post("#{resource_path}/#{id}/deactivate", body: {})
67
+ end
68
+
69
+ # Disconnect a channel (must be inactive first)
70
+ #
71
+ # @param id [String] Channel UUID
72
+ # @return [Response]
73
+ def disconnect(id)
74
+ delete(id)
75
+ end
76
+
77
+ # Full sync a channel (refresh all data from OTA)
78
+ #
79
+ # @param id [String] Channel UUID
80
+ # @return [Response]
81
+ def full_sync(id)
82
+ client.post("#{resource_path}/#{id}/full_sync", body: {})
83
+ end
84
+
85
+ protected
86
+
87
+ def resource_path
88
+ "/channels"
89
+ end
90
+
91
+ def filter_keys
92
+ %i[property_id]
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,29 @@
1
+ module Channex
2
+ module Resources
3
+ # Facilities API resource
4
+ #
5
+ # Facilities represent amenities available at properties or room types.
6
+ # This is a read-only resource providing the list of available facility IDs.
7
+ #
8
+ # @example List available facilities
9
+ # client.facilities.list
10
+ #
11
+ # @see https://docs.channex.io/api-v.1-documentation/facilities-collection
12
+ #
13
+ class Facilities < Resource
14
+ # List available facilities
15
+ #
16
+ # @param options [Hash] Additional options
17
+ # @return [Response]
18
+ def list(**options)
19
+ client.get(resource_path, params: options)
20
+ end
21
+
22
+ protected
23
+
24
+ def resource_path
25
+ "/facilities"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,76 @@
1
+ module Channex
2
+ module Resources
3
+ # Group Users API resource
4
+ #
5
+ # Group Users represent the association between users and groups,
6
+ # defining access permissions. By inviting a User into a Group, you
7
+ # automatically grant access for this user to all properties in the Group.
8
+ #
9
+ # @example List users in a group
10
+ # client.group_users.list(group_id: "group-uuid")
11
+ #
12
+ # @example Invite a user to a group
13
+ # client.group_users.invite(
14
+ # group_id: "group-uuid",
15
+ # user_email: "user@example.com",
16
+ # role: "user"
17
+ # )
18
+ #
19
+ # @see https://docs.channex.io/api-v.1-documentation/group-users-collection
20
+ #
21
+ class GroupUsers < Resource
22
+ ROLES = %w[owner user].freeze
23
+
24
+ # Invite a user to a group
25
+ #
26
+ # If the user is not registered, Channex will create their account
27
+ # automatically and send an email with instructions to onboard.
28
+ #
29
+ # @param group_id [String] Group UUID (required)
30
+ # @param user_email [String] Email address of the user to invite (required)
31
+ # @param role [String] Role: "owner" or "user" (required)
32
+ # @param overrides [Hash] Access policies overrides (optional)
33
+ # @return [Response]
34
+ #
35
+ # @example Invite a user with default permissions
36
+ # client.group_users.invite(
37
+ # group_id: "52397a6e-c330-44f4-a293-47042d3a3607",
38
+ # user_email: "other_user@channex.io",
39
+ # role: "user"
40
+ # )
41
+ def invite(group_id:, user_email:, role:, overrides: {})
42
+ client.post(resource_path, body: {
43
+ invite: {
44
+ group_id: group_id,
45
+ user_email: user_email,
46
+ role: role,
47
+ overrides: overrides
48
+ }
49
+ })
50
+ end
51
+
52
+ # @see #invite
53
+ def create(attributes)
54
+ invite(**attributes)
55
+ end
56
+
57
+ # Revoke a user's access to a group
58
+ #
59
+ # @param id [String] Group User UUID
60
+ # @return [Response]
61
+ def revoke(id)
62
+ delete(id)
63
+ end
64
+
65
+ protected
66
+
67
+ def resource_path
68
+ "/group_users"
69
+ end
70
+
71
+ def filter_keys
72
+ %i[group_id]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,26 @@
1
+ module Channex
2
+ module Resources
3
+ # Groups API resource
4
+ #
5
+ # Groups are used to organize properties and control access.
6
+ # Each property can have a Base Group assigned.
7
+ #
8
+ # @example List groups
9
+ # client.groups.list
10
+ #
11
+ # @example Create a group
12
+ # client.groups.create(
13
+ # title: "Europe Hotels"
14
+ # )
15
+ #
16
+ # @see https://docs.channex.io/api-v.1-documentation/groups-collection
17
+ #
18
+ class Groups < Resource
19
+ protected
20
+
21
+ def resource_path
22
+ "/groups"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ module Channex
2
+ module Resources
3
+ # Hotel Policies API resource
4
+ #
5
+ # Hotel Policies contain property-level settings like check-in/check-out times,
6
+ # internet access, parking, pets policy, and smoking rules.
7
+ #
8
+ # @example List hotel policies for a property
9
+ # client.hotel_policies.list(property_id: "property-uuid")
10
+ #
11
+ # @example Create a hotel policy
12
+ # client.hotel_policies.create(
13
+ # property_id: "property-uuid",
14
+ # title: "Standard Policy",
15
+ # currency: "USD",
16
+ # checkin_time: "15:00",
17
+ # checkout_time: "11:00",
18
+ # pets_policy: "not_allowed",
19
+ # smoking_policy: "no_smoking"
20
+ # )
21
+ #
22
+ # @see https://docs.channex.io/api-v.1-documentation/hotel-policy-collection
23
+ #
24
+ class HotelPolicies < Resource
25
+ # Pets policy options
26
+ PETS_POLICIES = %w[allowed not_allowed by_arrangements pets_not_allowed_on_beds].freeze
27
+
28
+ # Smoking policy options
29
+ SMOKING_POLICIES = %w[no_smoking smoking_allowed designated_areas].freeze
30
+
31
+ # Internet access type options
32
+ INTERNET_ACCESS_TYPES = %w[wifi wired none].freeze
33
+
34
+ # Internet access coverage options
35
+ INTERNET_ACCESS_COVERAGES = %w[entire_property public_areas some_rooms all_rooms].freeze
36
+
37
+ # Parking type options
38
+ PARKING_TYPES = %w[on_site off_site none].freeze
39
+
40
+ # Parking reservation options
41
+ PARKING_RESERVATIONS = %w[needed not_needed not_available].freeze
42
+
43
+ protected
44
+
45
+ def resource_path
46
+ "/hotel_policies"
47
+ end
48
+
49
+ def filter_keys
50
+ %i[property_id]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,89 @@
1
+ module Channex
2
+ module Resources
3
+ # Message Threads API resource
4
+ #
5
+ # A message thread is one guest conversation, normalised by Channex across
6
+ # Booking.com, Airbnb and Expedia. Threads are the channel-agnostic entry
7
+ # point: they exist even when there is no booking to hang them off (an
8
+ # Airbnb inquiry), which booking-scoped messages cannot represent.
9
+ #
10
+ # Requires the Messages application to be installed on the property —
11
+ # without it every call in here answers 403 Forbidden.
12
+ #
13
+ # @example List the open threads for a property
14
+ # client.message_threads.list(property_id: "property-uuid")
15
+ #
16
+ # @example Read a conversation
17
+ # client.message_threads.messages("thread-uuid", order: {inserted_at: :asc})
18
+ #
19
+ # @example Reply to the guest
20
+ # client.message_threads.send_message("thread-uuid", message: "Check-in is from 15:00.")
21
+ #
22
+ # @see https://docs.channex.io/api-v.1-documentation/messages-collection
23
+ #
24
+ class MessageThreads < Resource
25
+ # List the messages on a thread, newest first by default
26
+ #
27
+ # @param id [String] Thread UUID
28
+ # @param options [Hash] Additional options (page, limit, filter, order)
29
+ # @return [Response]
30
+ def messages(id, **options)
31
+ client.get("#{resource_path}/#{id}/messages", params: query_params(**options))
32
+ end
33
+
34
+ # Send a message to the guest on a thread
35
+ #
36
+ # Pass `attachment_id` to send a file previously uploaded through
37
+ # {Attachments#create}; `message` and `attachment_id` can be combined to
38
+ # send a file with a caption.
39
+ #
40
+ # @param id [String] Thread UUID
41
+ # @param message [String] Message text
42
+ # @param attachment_id [String] Attachment UUID from {Attachments#create}
43
+ # @param attributes [Hash] Additional message attributes
44
+ # @return [Response]
45
+ def send_message(id, message: nil, attachment_id: nil, **attributes)
46
+ client.post(
47
+ "#{resource_path}/#{id}/messages",
48
+ body: {message: message_attributes(message:, attachment_id:, **attributes)}
49
+ )
50
+ end
51
+
52
+ # Close a thread
53
+ #
54
+ # @param id [String] Thread UUID
55
+ # @return [Response]
56
+ def close(id)
57
+ client.post("#{resource_path}/#{id}/close", body: {})
58
+ end
59
+
60
+ # Mark a thread as needing no reply (Booking.com only)
61
+ #
62
+ # Booking.com scores properties on how fast they answer, but some guest
63
+ # messages do not call for an answer at all. Marking the thread keeps the
64
+ # response-time score honest instead of leaving the clock running.
65
+ #
66
+ # @param id [String] Thread UUID
67
+ # @return [Response]
68
+ def no_reply_needed(id)
69
+ client.post("#{resource_path}/#{id}/no_reply_needed", body: {})
70
+ end
71
+
72
+ protected
73
+
74
+ def resource_path
75
+ "/message_threads"
76
+ end
77
+
78
+ def filter_keys
79
+ %i[property_id booking_id]
80
+ end
81
+
82
+ def message_attributes(message: nil, attachment_id: nil, **attributes)
83
+ attributes[:message] = message if message
84
+ attributes[:attachment_id] = attachment_id if attachment_id
85
+ attributes
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,64 @@
1
+ module Channex
2
+ module Resources
3
+ # Booking Messages API resource
4
+ #
5
+ # The booking-scoped view of the same conversations {MessageThreads}
6
+ # exposes: reach a guest by the booking you already hold, without first
7
+ # resolving which thread it belongs to. Channex answers 422 `not_supported`
8
+ # for bookings from an OTA that has no messaging API.
9
+ #
10
+ # Requires the Messages application to be installed on the property.
11
+ #
12
+ # @example Read the conversation on a booking
13
+ # client.messages.list(booking_id: "booking-uuid")
14
+ #
15
+ # @example Answer the guest
16
+ # client.messages.send_message(
17
+ # booking_id: "booking-uuid",
18
+ # message: "Thank you for your reservation!"
19
+ # )
20
+ #
21
+ # @see https://docs.channex.io/api-v.1-documentation/messages-collection
22
+ #
23
+ class Messages < Resource
24
+ # List the messages on a booking, newest first by default
25
+ #
26
+ # @param booking_id [String] Booking UUID
27
+ # @param options [Hash] Additional options (page, limit, filter, order)
28
+ # @return [Response]
29
+ def list(booking_id:, **options)
30
+ client.get(booking_path(booking_id), params: query_params(**options))
31
+ end
32
+
33
+ # Send a message to the guest on a booking
34
+ #
35
+ # Pass `attachment_id` to send a file previously uploaded through
36
+ # {Attachments#create}; `message` and `attachment_id` can be combined to
37
+ # send a file with a caption.
38
+ #
39
+ # @param booking_id [String] Booking UUID
40
+ # @param message [String] Message text
41
+ # @param attachment_id [String] Attachment UUID from {Attachments#create}
42
+ # @param attributes [Hash] Additional message attributes
43
+ # @return [Response]
44
+ def send_message(booking_id:, message: nil, attachment_id: nil, **attributes)
45
+ attributes[:message] = message if message
46
+ attributes[:attachment_id] = attachment_id if attachment_id
47
+
48
+ client.post(booking_path(booking_id), body: {message: attributes})
49
+ end
50
+
51
+ protected
52
+
53
+ # Messages only exist under a booking, so there is no collection of their
54
+ # own to retrieve, update or delete.
55
+ def resource_path
56
+ raise NotImplementedError, "Messages are scoped to a booking — use #list or #send_message"
57
+ end
58
+
59
+ def booking_path(booking_id)
60
+ "/bookings/#{booking_id}/messages"
61
+ end
62
+ end
63
+ end
64
+ end