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,76 @@
1
+ module Channex
2
+ module Objects
3
+ # A conversation between a guest and the property, as Channex unifies it
4
+ # across Booking.com, Airbnb and Expedia.
5
+ #
6
+ # Like {Message}, a thread arrives as a JSON:API resource node;
7
+ # {.from_resource} flattens the identifier and relationships into the
8
+ # attributes so the whole thread is one hash.
9
+ #
10
+ # A thread usually belongs to a booking, but not always: an Airbnb inquiry
11
+ # is a conversation about a stay that does not exist yet, so `booking_id`
12
+ # is nil until the guest books.
13
+ #
14
+ # @example
15
+ # thread.title # => "Maldonado Roxy"
16
+ # thread.provider # => "BookingCom"
17
+ # thread.closed? # => false
18
+ # thread.last_message # => Channex::Objects::Message
19
+ #
20
+ class MessageThread < Channex::Object
21
+ attribute :id
22
+ attribute :title
23
+ attribute :provider
24
+ attribute :message_count
25
+ attribute :last_message, type: Message
26
+ attribute :last_message_received_at
27
+ attribute :inserted_at
28
+ attribute :updated_at
29
+
30
+ # Lifted out of `relationships`
31
+ attribute :booking_id
32
+ attribute :channel_id
33
+ attribute :property_id
34
+
35
+ # Flatten a JSON:API resource node into a thread
36
+ #
37
+ # @param node [Hash, nil] A `{"id", "attributes", "relationships"}` node
38
+ # @return [MessageThread, nil]
39
+ def self.from_resource(node)
40
+ return nil if node.nil?
41
+
42
+ attributes = node["attributes"] || {}
43
+ relationships = node["relationships"] || {}
44
+
45
+ new(
46
+ attributes.merge(
47
+ "id" => node["id"],
48
+ "booking_id" => relationships.dig("booking", "data", "id"),
49
+ "channel_id" => relationships.dig("channel", "data", "id"),
50
+ "property_id" => relationships.dig("property", "data", "id")
51
+ ).compact
52
+ )
53
+ end
54
+
55
+ # Whether the property closed the thread. Closed threads still accept
56
+ # messages; the flag is an inbox state, not a lock.
57
+ #
58
+ # @return [Boolean]
59
+ def closed?
60
+ @data["is_closed"] == true
61
+ end
62
+
63
+ # @return [Boolean]
64
+ def open?
65
+ !closed?
66
+ end
67
+
68
+ # Whether the thread hangs off a booking. Airbnb inquiries do not.
69
+ #
70
+ # @return [Boolean]
71
+ def booking?
72
+ !booking_id.nil?
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,29 @@
1
+ module Channex
2
+ module Objects
3
+ # Guest occupancy information
4
+ #
5
+ # @example
6
+ # occupancy.adults # => 2
7
+ # occupancy.children # => 1
8
+ # occupancy.infants # => 0
9
+ # occupancy.total_guests # => 3
10
+ #
11
+ class Occupancy < Channex::Object
12
+ attribute :adults
13
+ attribute :children
14
+ attribute :infants
15
+
16
+ # Child ages array (when provided)
17
+ # @return [Array<Integer>]
18
+ def ages
19
+ @data["ages"] || []
20
+ end
21
+
22
+ # Total number of guests
23
+ # @return [Integer]
24
+ def total_guests
25
+ (adults || 0) + (children || 0) + (infants || 0)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,87 @@
1
+ module Channex
2
+ module Objects
3
+ # A guest's review of a stay, as Channex unifies it across Booking.com,
4
+ # Airbnb and Expedia.
5
+ #
6
+ # Like {MessageThread}, a review arrives as a JSON:API resource node;
7
+ # {.from_resource} flattens the identifier and relationships into the
8
+ # attributes so the whole review is one hash.
9
+ #
10
+ # Scores are out of 10 whatever the OTA rates on; `scores` holds one
11
+ # `{"category", "score"}` pair per category the OTA asks about, named in
12
+ # the OTA's own vocabulary (`clean` on Booking.com, `cleanliness` on Airbnb).
13
+ #
14
+ # @example
15
+ # review.ota # => "BookingCom"
16
+ # review.overall_score # => 9.0
17
+ # review.scores # => [{"category" => "clean", "score" => 10}]
18
+ # review.replied? # => false
19
+ #
20
+ class Review < Channex::Object
21
+ attribute :id
22
+ attribute :content
23
+ attribute :guest_name
24
+ attribute :ota
25
+ attribute :ota_reservation_id
26
+ attribute :overall_score
27
+ attribute :reply
28
+ attribute :received_at
29
+ attribute :inserted_at
30
+ attribute :updated_at
31
+
32
+ # Lifted out of `relationships`
33
+ attribute :booking_id
34
+ attribute :channel_id
35
+ attribute :property_id
36
+
37
+ # Flatten a JSON:API resource node into a review
38
+ #
39
+ # @param node [Hash, nil] A `{"id", "attributes", "relationships"}` node
40
+ # @return [Review, nil]
41
+ def self.from_resource(node)
42
+ return nil if node.nil?
43
+
44
+ attributes = node["attributes"] || {}
45
+ relationships = node["relationships"] || {}
46
+
47
+ new(
48
+ attributes.merge(
49
+ "id" => node["id"],
50
+ "booking_id" => relationships.dig("booking", "data", "id"),
51
+ "channel_id" => relationships.dig("channel", "data", "id"),
52
+ "property_id" => relationships.dig("property", "data", "id")
53
+ ).compact
54
+ )
55
+ end
56
+
57
+ # Per-category scores, `{"category", "score"}` pairs
58
+ #
59
+ # @return [Array<Hash>]
60
+ def scores
61
+ @data["scores"] || []
62
+ end
63
+
64
+ # Review tag codes (Airbnb only)
65
+ #
66
+ # @return [Array<String>]
67
+ def tags
68
+ @data["tags"] || []
69
+ end
70
+
71
+ # Whether the property has replied
72
+ #
73
+ # @return [Boolean]
74
+ def replied?
75
+ @data["is_replied"] == true
76
+ end
77
+
78
+ # Whether Airbnb is holding the review back until the host reviews the
79
+ # guest in turn (Airbnb only)
80
+ #
81
+ # @return [Boolean]
82
+ def hidden?
83
+ @data["is_hidden"] == true
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,44 @@
1
+ module Channex
2
+ module Objects
3
+ # Additional service, fee, or extra sold alongside a booking.
4
+ #
5
+ # Channex represents meals, fees, and extras uniformly in a booking's
6
+ # `services` array (present at both booking and room level), distinguished
7
+ # by `type`. A fee charged on a cancelled booking also arrives here — note
8
+ # that real OTA payloads use a specific `type` (e.g. Booking.com sends
9
+ # "Cancellation Fee"), not the generic "Fee" the API docs imply.
10
+ #
11
+ # `is_inclusive` indicates whether tax is already baked into the amount.
12
+ #
13
+ # @see https://docs.channex.io/api-v.1-documentation/bookings-collection
14
+ #
15
+ # @example
16
+ # service.type # => "Cancellation Fee"
17
+ # service.name # => "Cancellation Fee"
18
+ # service.total_price # => "12240.00" (total amount for the service)
19
+ # service.price_per_unit # => "12240.00"
20
+ # service.price_mode # => "Per stay"
21
+ # service.nights # => 0
22
+ # service.persons # => 0
23
+ # service.inclusive? # => true
24
+ # service.room_index # => 0 (room the service is associated with, if any)
25
+ #
26
+ class Service < Channex::Object
27
+ attribute :type
28
+ attribute :name
29
+ attribute :total_price
30
+ attribute :price_per_unit
31
+ attribute :price_mode
32
+ attribute :persons
33
+ attribute :nights
34
+ attribute :room_index
35
+ attribute :inclusive, key: "is_inclusive"
36
+
37
+ # Whether tax is already included in the amount
38
+ # @return [Boolean]
39
+ def inclusive?
40
+ inclusive == true
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ module Channex
2
+ module Objects
3
+ # Tax applied to a booking room.
4
+ #
5
+ # Channex represents taxes with the same shape as services: an amount
6
+ # (`total_price` / `price_per_unit`), a `price_mode`, and an `is_inclusive`
7
+ # flag indicating whether the tax is already baked into the room price.
8
+ #
9
+ # @see https://docs.channex.io/api-v.1-documentation/taxes-and-tax-sets
10
+ #
11
+ # @example
12
+ # tax.type # => "Value Added Tax (VAT)"
13
+ # tax.name # => "VAT (12%)"
14
+ # tax.total_price # => "1311.43"
15
+ # tax.price_per_unit # => "1311.43"
16
+ # tax.price_mode # => "Per booking"
17
+ # tax.inclusive? # => true
18
+ #
19
+ class Tax < Channex::Object
20
+ attribute :type
21
+ attribute :name
22
+ attribute :total_price
23
+ attribute :price_per_unit
24
+ attribute :price_mode
25
+ attribute :persons
26
+ attribute :nights
27
+ attribute :inclusive, key: "is_inclusive"
28
+
29
+ # Whether the tax is already included in the price
30
+ # @return [Boolean]
31
+ def inclusive?
32
+ inclusive == true
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ module Channex
2
+ # Namespace for typed API response objects
3
+ #
4
+ # All objects inherit from Channex::Object and provide:
5
+ # - Typed attribute accessors
6
+ # - Hash compatibility via [], dig, to_h
7
+ # - Nested object wrapping
8
+ #
9
+ module Objects
10
+ end
11
+ end
12
+
13
+ # Load in dependency order (leaf nodes first)
14
+ require_relative "objects/company"
15
+ require_relative "objects/occupancy"
16
+ require_relative "objects/guest"
17
+ require_relative "objects/tax"
18
+ require_relative "objects/service"
19
+ require_relative "objects/guarantee"
20
+ require_relative "objects/customer"
21
+ require_relative "objects/booking_room"
22
+ require_relative "objects/booking_revision"
@@ -0,0 +1,130 @@
1
+ module Channex
2
+ # Base class for API resources
3
+ #
4
+ # Provides common CRUD operations and pagination support.
5
+ # Subclasses should define their resource path and customize
6
+ # behavior as needed.
7
+ #
8
+ class Resource
9
+ attr_reader :client
10
+
11
+ def initialize(client)
12
+ @client = client
13
+ end
14
+
15
+ # List all resources with optional filtering and pagination
16
+ #
17
+ # @param page [Integer] Page number (starts at 1)
18
+ # @param limit [Integer] Number of results per page (max 100)
19
+ # @param filter [Hash] Filter parameters
20
+ # @param order [Hash] Ordering parameters (e.g., { inserted_at: :asc })
21
+ # @param params [Hash] Keywords named by #filter_keys join the filter; the rest are sent as top-level params
22
+ # @return [Response]
23
+ def list(page: nil, limit: nil, filter: {}, order: {}, **params)
24
+ named_filters = params.extract!(*filter_keys).compact
25
+ filter = filter.merge(named_filters)
26
+ client.get(resource_path, params: query_params(page:, limit:, filter:, order:, **params))
27
+ end
28
+
29
+ # Retrieve all resources using automatic pagination
30
+ #
31
+ # @yield [data] Yields each page of data
32
+ # @yieldparam data [Array] Array of resource data from the page
33
+ # @return [Array] All resources if no block given
34
+ def list_all(filter: {}, order: {}, **params)
35
+ all_data = []
36
+ page = 1
37
+ limit = 100
38
+
39
+ loop do
40
+ response = list(page: page, limit: limit, filter: filter, order: order, **params)
41
+ data = response.data || []
42
+
43
+ if block_given?
44
+ yield data
45
+ else
46
+ all_data.concat(data)
47
+ end
48
+
49
+ break unless response.pagination.has_next?
50
+
51
+ page += 1
52
+ end
53
+
54
+ all_data unless block_given?
55
+ end
56
+
57
+ # Retrieve a single resource by ID
58
+ #
59
+ # @param id [String] Resource UUID
60
+ # @return [Response]
61
+ def retrieve(id)
62
+ client.get("#{resource_path}/#{id}")
63
+ end
64
+
65
+ # Create a new resource
66
+ #
67
+ # @param attributes [Hash] Resource attributes
68
+ # @return [Response]
69
+ def create(attributes)
70
+ client.post(resource_path, body: wrap_attributes(attributes))
71
+ end
72
+
73
+ # Update an existing resource
74
+ #
75
+ # @param id [String] Resource UUID
76
+ # @param attributes [Hash] Resource attributes to update
77
+ # @return [Response]
78
+ def update(id, attributes)
79
+ client.put("#{resource_path}/#{id}", body: wrap_attributes(attributes))
80
+ end
81
+
82
+ # Delete a resource
83
+ #
84
+ # @param id [String] Resource UUID
85
+ # @return [Response]
86
+ def delete(id)
87
+ client.delete("#{resource_path}/#{id}")
88
+ end
89
+
90
+ protected
91
+
92
+ # Build the query string Channex expects: pagination, filter and order are
93
+ # each nested one level deep (e.g. `pagination[page]=2`). Shared with
94
+ # resources whose collections hang off another resource's path.
95
+ #
96
+ # @return [Hash]
97
+ def query_params(page: nil, limit: nil, filter: {}, order: {}, **params)
98
+ query = params.dup
99
+
100
+ pagination = {}
101
+ pagination[:page] = page if page
102
+ pagination[:limit] = limit if limit
103
+ query[:pagination] = pagination unless pagination.empty?
104
+
105
+ query[:filter] = filter unless filter.empty?
106
+ query[:order] = order unless order.empty?
107
+
108
+ query
109
+ end
110
+
111
+ # Override in subclasses to define the API path
112
+ def resource_path
113
+ raise NotImplementedError, "Subclass must implement #resource_path"
114
+ end
115
+
116
+ # Override to change how attributes are wrapped for create/update
117
+ def wrap_attributes(attributes)
118
+ {resource_name => attributes}
119
+ end
120
+
121
+ def resource_name
122
+ self.class.name.demodulize.underscore.singularize
123
+ end
124
+
125
+ # Override to name the keywords #list sends to Channex as filters
126
+ def filter_keys
127
+ []
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,36 @@
1
+ module Channex
2
+ module Resources
3
+ # Attachments API resource
4
+ #
5
+ # Sending a file to a guest is two calls: upload it here, then pass the
6
+ # returned id to {MessageThreads#send_message} or {Messages#send_message}.
7
+ #
8
+ # @example Send a photo of the directions
9
+ # attachment = client.attachments.create(
10
+ # file: Base64.strict_encode64(File.binread("directions.jpeg")),
11
+ # file_name: "directions.jpeg",
12
+ # file_type: "image/jpeg"
13
+ # )
14
+ # client.message_threads.send_message("thread-uuid", attachment_id: attachment.data["id"])
15
+ #
16
+ # @see https://docs.channex.io/api-v.1-documentation/messages-collection
17
+ #
18
+ class Attachments < Resource
19
+ # Upload an attachment
20
+ #
21
+ # @param file [String] Base64-encoded file contents
22
+ # @param file_name [String] File name, e.g. "photo.jpeg"
23
+ # @param file_type [String] MIME type, e.g. "image/jpeg"
24
+ # @return [Response]
25
+ def create(file:, file_name:, file_type:)
26
+ client.post(resource_path, body: {attachment: {file:, file_name:, file_type:}})
27
+ end
28
+
29
+ protected
30
+
31
+ def resource_path
32
+ "/attachments"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,87 @@
1
+ module Channex
2
+ module Resources
3
+ # Availability API resource
4
+ #
5
+ # Availability represents how many rooms your property has to sell.
6
+ # This is the core data for inventory management in Channex.
7
+ #
8
+ # @example Get availability for a property
9
+ # client.availability.list(
10
+ # property_id: "property-uuid",
11
+ # date_from: "2024-01-01",
12
+ # date_to: "2024-01-31"
13
+ # )
14
+ #
15
+ # @example Update availability for specific dates
16
+ # client.availability.update([
17
+ # {
18
+ # property_id: "property-uuid",
19
+ # room_type_id: "room-uuid",
20
+ # date_from: "2024-01-15",
21
+ # date_to: "2024-01-20",
22
+ # availability: 5
23
+ # }
24
+ # ])
25
+ #
26
+ # @see https://docs.channex.io/api-v.1-documentation/ari
27
+ #
28
+ class Availability < Resource
29
+ # Get availability for a property within a date range
30
+ #
31
+ # @param property_id [String] Property UUID (required)
32
+ # @param date [String] Specific date (YYYY-MM-DD)
33
+ # @param date_from [String] Start date (YYYY-MM-DD), uses filter[date][gte]
34
+ # @param date_to [String] End date (YYYY-MM-DD), uses filter[date][lte]
35
+ # @param options [Hash] Additional options
36
+ # @return [Response]
37
+ def list(property_id:, date: nil, date_from: nil, date_to: nil, **options)
38
+ filter = options.delete(:filter) || {}
39
+ filter[:property_id] = property_id
40
+
41
+ if date
42
+ filter[:date] = date
43
+ else
44
+ filter[:date] = {}
45
+ filter[:date][:gte] = date_from if date_from
46
+ filter[:date][:lte] = date_to if date_to
47
+ end
48
+
49
+ client.get(resource_path, params: {filter: filter, **options})
50
+ end
51
+
52
+ # Update availability for properties
53
+ #
54
+ # Each value in the array should contain all required fields including property_id.
55
+ # You can update multiple properties and room types in a single request.
56
+ #
57
+ # @param values [Array<Hash>] Array of availability updates
58
+ # Each hash should contain:
59
+ # - property_id [String] Property UUID (required)
60
+ # - room_type_id [String] Room Type UUID (required)
61
+ # - date_from [String] Start date YYYY-MM-DD (required)
62
+ # - date_to [String] End date YYYY-MM-DD (required)
63
+ # - availability [Integer] Number of available rooms (required)
64
+ # @return [Response]
65
+ #
66
+ # @example Update availability for a date range
67
+ # client.availability.update([
68
+ # {
69
+ # property_id: "716305c4-561a-4561-a187-7f5b8aeb5920",
70
+ # room_type_id: "994d1375-dbbd-4072-8724-b2ab32ce781b",
71
+ # date_from: "2024-01-15",
72
+ # date_to: "2024-01-20",
73
+ # availability: 5
74
+ # }
75
+ # ])
76
+ def update(values)
77
+ client.post(resource_path, body: {values: values})
78
+ end
79
+
80
+ protected
81
+
82
+ def resource_path
83
+ "/availability"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,52 @@
1
+ module Channex
2
+ module Resources
3
+ # Availability Rules API resource
4
+ #
5
+ # Availability Rules allow you to create dynamic rules that affect
6
+ # availability for specific channels, room types, and date ranges.
7
+ # Rules can close out availability, set offsets, or cap maximum availability.
8
+ #
9
+ # @example List availability rules for a property
10
+ # client.availability_rules.list(property_id: "property-uuid")
11
+ #
12
+ # @example Create a close-out rule for specific dates
13
+ # client.availability_rules.create(
14
+ # property_id: "property-uuid",
15
+ # affected_channels: ["channel-uuid"],
16
+ # affected_room_types: ["room-type-uuid"],
17
+ # type: "close_out",
18
+ # start_date: "2024-12-24",
19
+ # end_date: "2024-12-26"
20
+ # )
21
+ #
22
+ # @example Create an availability offset rule
23
+ # client.availability_rules.create(
24
+ # property_id: "property-uuid",
25
+ # affected_channels: ["channel-uuid"],
26
+ # affected_room_types: ["room-type-uuid"],
27
+ # type: "availability_offset",
28
+ # value: 2, # Reserve 2 rooms from OTA availability
29
+ # start_date: "2024-01-01"
30
+ # )
31
+ #
32
+ # @see https://docs.channex.io/api-v.1-documentation/availability-rules-collection
33
+ #
34
+ class AvailabilityRules < Resource
35
+ # Rule types
36
+ TYPES = %w[close_out availability_offset max_availability].freeze
37
+
38
+ # Days of week
39
+ DAYS = %w[mo tu we th fr sa su].freeze
40
+
41
+ protected
42
+
43
+ def resource_path
44
+ "/availability_rules"
45
+ end
46
+
47
+ def filter_keys
48
+ %i[property_id]
49
+ end
50
+ end
51
+ end
52
+ end