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,62 @@
1
+ module Channex
2
+ # Error classes for Channex API responses
3
+ module Errors
4
+ # Base error class for all Channex errors
5
+ class Error < StandardError
6
+ attr_reader :response, :code
7
+
8
+ def initialize(message = nil, response: nil, code: nil)
9
+ @response = response
10
+ @code = code
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ # Raised when authentication fails (401)
16
+ class AuthenticationError < Error; end
17
+
18
+ # Raised when the request is forbidden (403)
19
+ class ForbiddenError < Error; end
20
+
21
+ # Raised when a resource is not found (404)
22
+ class NotFoundError < Error; end
23
+
24
+ # Raised when validation fails (422)
25
+ class ValidationError < Error
26
+ attr_reader :errors
27
+
28
+ # Channex details the rejected fields by name. Only the names join the message, which reaches error
29
+ # tracking: the details can echo the values sent.
30
+ def initialize(message = nil, errors: {}, **kwargs)
31
+ @errors = errors
32
+ fields = errors.is_a?(Hash) ? errors.keys : []
33
+ super(fields.any? ? "#{message}: #{fields.join(", ")}" : message, **kwargs)
34
+ end
35
+ end
36
+
37
+ # Raised when rate limited (429)
38
+ class RateLimitError < Error
39
+ attr_reader :retry_after
40
+
41
+ def initialize(message = nil, retry_after: nil, **kwargs)
42
+ @retry_after = retry_after
43
+ super(message, **kwargs)
44
+ end
45
+ end
46
+
47
+ # Raised for server errors (5xx)
48
+ class ServerError < Error; end
49
+
50
+ # Raised for connection/network errors
51
+ class ConnectionError < Error; end
52
+
53
+ # Raised when request times out
54
+ class TimeoutError < Error; end
55
+
56
+ # Raised for invalid configuration
57
+ class ConfigurationError < Error; end
58
+
59
+ # Errors that repeating the same request can't fix.
60
+ PERMANENT = [AuthenticationError, ForbiddenError, NotFoundError, ValidationError].freeze
61
+ end
62
+ end
@@ -0,0 +1,201 @@
1
+ module Channex
2
+ # Base class for typed API response objects
3
+ #
4
+ # Provides typed accessor methods while maintaining hash compatibility
5
+ # for backwards compatibility and easy data exploration.
6
+ #
7
+ # @example Basic usage
8
+ # revision = Channex::Objects::BookingRevision.new(data)
9
+ # revision.id # => "uuid" (typed accessor)
10
+ # revision["id"] # => "uuid" (hash access)
11
+ # revision.dig("customer", "name") # => "John"
12
+ # revision.to_h # => {"id" => "uuid", ...}
13
+ #
14
+ # @example Defining attributes in subclasses
15
+ # class BookingRevision < Channex::Object
16
+ # attribute :id
17
+ # attribute :customer, type: Customer
18
+ # array_attribute :rooms, type: BookingRoom
19
+ # end
20
+ #
21
+ class Object
22
+ include Enumerable
23
+
24
+ class << self
25
+ # Define a typed attribute with optional type coercion
26
+ #
27
+ # @param name [Symbol] Attribute name (becomes the method name)
28
+ # @param type [Class, nil] Optional class for nested object wrapping
29
+ # @param key [String, nil] Override the hash key (defaults to name.to_s)
30
+ #
31
+ # @example Simple attribute
32
+ # attribute :id
33
+ #
34
+ # @example Nested object
35
+ # attribute :customer, type: Customer
36
+ #
37
+ # @example Different key name
38
+ # attribute :email, key: "mail"
39
+ #
40
+ def attribute(name, type: nil, key: nil)
41
+ key ||= name.to_s
42
+
43
+ attributes[name] = {key: key, type: type}
44
+
45
+ define_method(name) do
46
+ value = @data[key]
47
+ return nil if value.nil?
48
+
49
+ if type
50
+ @wrapped_values[name] ||= wrap_value(value, type)
51
+ else
52
+ value
53
+ end
54
+ end
55
+ end
56
+
57
+ # Define an array attribute with typed elements
58
+ #
59
+ # @param name [Symbol] Attribute name
60
+ # @param type [Class] Class for array element wrapping
61
+ # @param key [String, nil] Override the hash key
62
+ #
63
+ # @example
64
+ # array_attribute :rooms, type: BookingRoom
65
+ #
66
+ def array_attribute(name, type:, key: nil)
67
+ key ||= name.to_s
68
+
69
+ attributes[name] = {key: key, type: type, array: true}
70
+
71
+ define_method(name) do
72
+ @wrapped_values[name] ||= begin
73
+ raw = @data[key] || []
74
+ raw.map { |item| wrap_value(item, type) }
75
+ end
76
+ end
77
+ end
78
+
79
+ # Registry of defined attributes for this class
80
+ # @return [Hash]
81
+ def attributes
82
+ @attributes ||= {}
83
+ end
84
+
85
+ # Construct an object from raw API data
86
+ #
87
+ # @param data [Hash, nil] Raw API response data
88
+ # @return [Object, nil] Wrapped object or nil if data is nil
89
+ def construct_from(data)
90
+ return nil if data.nil?
91
+ new(data)
92
+ end
93
+ end
94
+
95
+ # @param data [Hash] Raw API response data
96
+ def initialize(data)
97
+ @data = data.is_a?(Hash) ? data : {}
98
+ @wrapped_values = {}
99
+ end
100
+
101
+ # Hash-like access for backwards compatibility
102
+ #
103
+ # @param key [String, Symbol] Hash key
104
+ # @return [Object, nil] Value at key
105
+ def [](key)
106
+ @data[key.to_s]
107
+ end
108
+
109
+ # Deep access for nested data
110
+ #
111
+ # @param keys [Array<String, Symbol>] Path of keys
112
+ # @return [Object, nil] Value at path
113
+ def dig(*keys)
114
+ @data.dig(*keys.map(&:to_s))
115
+ end
116
+
117
+ # Return the underlying hash data
118
+ # @return [Hash]
119
+ def to_h
120
+ @data.dup
121
+ end
122
+ alias_method :to_hash, :to_h
123
+
124
+ # JSON serialization
125
+ # @return [String]
126
+ def to_json(*args)
127
+ @data.to_json(*args)
128
+ end
129
+
130
+ # Rails JSON serialization support
131
+ # @return [Hash]
132
+ def as_json(*args)
133
+ @data.as_json(*args)
134
+ end
135
+
136
+ # Enumerable support - iterate over key/value pairs
137
+ def each(&block)
138
+ @data.each(&block)
139
+ end
140
+
141
+ # @return [Array<String>] All keys in the data
142
+ delegate :keys, to: :@data
143
+
144
+ # @return [Array] All values in the data
145
+ delegate :values, to: :@data
146
+
147
+ # Check if a key exists
148
+ # @param key [String, Symbol]
149
+ # @return [Boolean]
150
+ def key?(key)
151
+ @data.key?(key.to_s)
152
+ end
153
+ alias_method :has_key?, :key?
154
+
155
+ # Fetch with default value support
156
+ # @param key [String, Symbol]
157
+ # @return [Object]
158
+ def fetch(key, *args, &block)
159
+ @data.fetch(key.to_s, *args, &block)
160
+ end
161
+
162
+ # Debug representation
163
+ # @return [String]
164
+ def inspect
165
+ "#<#{self.class.name} #{@data.inspect}>"
166
+ end
167
+
168
+ # @return [String]
169
+ def to_s
170
+ inspect
171
+ end
172
+
173
+ # Equality based on underlying data
174
+ # @param other [Object, Hash]
175
+ # @return [Boolean]
176
+ def ==(other)
177
+ case other
178
+ when Channex::Object
179
+ @data == other.to_h
180
+ when Hash
181
+ @data == other
182
+ else
183
+ false
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ def wrap_value(value, type)
190
+ return nil if value.nil?
191
+
192
+ if type.respond_to?(:construct_from)
193
+ type.construct_from(value)
194
+ elsif type.is_a?(Class) && type < Channex::Object
195
+ type.new(value)
196
+ else
197
+ value
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,124 @@
1
+ module Channex
2
+ module Objects
3
+ # Booking revision from Channex API
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
+ #
8
+ # @example Basic usage
9
+ # revision.id # => "uuid"
10
+ # revision.status # => "new"
11
+ # revision.new? # => true
12
+ # revision.customer.full_name # => "John Doe"
13
+ # revision.rooms.first.nights # => 3
14
+ #
15
+ # @example Hash access still works
16
+ # revision["booking_id"] # => "uuid"
17
+ # revision.dig("customer", "mail") # => "john@example.com"
18
+ #
19
+ class BookingRevision < Channex::Object
20
+ # Core identifiers
21
+ attribute :id
22
+ attribute :booking_id
23
+ attribute :property_id
24
+ attribute :unique_id
25
+ attribute :system_id
26
+
27
+ # OTA information
28
+ attribute :ota_name
29
+ attribute :ota_reservation_code
30
+
31
+ # Status and timing
32
+ attribute :status
33
+ attribute :inserted_at
34
+
35
+ # Booking dates
36
+ attribute :arrival_date
37
+ attribute :departure_date
38
+ attribute :arrival_hour
39
+
40
+ # Financial
41
+ attribute :amount
42
+ attribute :currency
43
+ attribute :ota_commission
44
+ attribute :payment_collect
45
+ attribute :payment_type
46
+
47
+ # Other
48
+ attribute :notes
49
+
50
+ # Nested objects
51
+ attribute :customer, type: Customer
52
+ attribute :occupancy, type: Occupancy
53
+ attribute :guarantee, type: Guarantee
54
+
55
+ # Arrays
56
+ array_attribute :rooms, type: BookingRoom
57
+ array_attribute :services, type: Service
58
+
59
+ # Status check helpers
60
+ # @return [Boolean]
61
+ def new?
62
+ status == "new"
63
+ end
64
+
65
+ # @return [Boolean]
66
+ def modified?
67
+ status == "modified"
68
+ end
69
+
70
+ # @return [Boolean]
71
+ def cancelled?
72
+ status == "cancelled"
73
+ end
74
+
75
+ # Arrival date as Date object
76
+ # @return [Date, nil]
77
+ def check_in_date
78
+ Date.parse(arrival_date) if arrival_date
79
+ end
80
+
81
+ # Departure date as Date object
82
+ # @return [Date, nil]
83
+ def check_out_date
84
+ Date.parse(departure_date) if departure_date
85
+ end
86
+
87
+ # Timestamp when revision was received
88
+ # @return [Time, nil]
89
+ def received_at
90
+ Time.parse(inserted_at) if inserted_at
91
+ end
92
+
93
+ # Total booking amount as Money object (if money gem is available)
94
+ # @return [Money, nil]
95
+ def total_amount
96
+ return nil unless amount && currency
97
+ return nil unless defined?(Money)
98
+
99
+ Money.from_amount(BigDecimal(amount.to_s), currency)
100
+ end
101
+
102
+ # OTA commission as Money object (if money gem is available)
103
+ # @return [Money, nil]
104
+ def commission_amount
105
+ return nil unless ota_commission && currency
106
+ return nil unless defined?(Money)
107
+
108
+ Money.from_amount(BigDecimal(ota_commission.to_s), currency)
109
+ end
110
+
111
+ # Whether payment should be collected by the property
112
+ # @return [Boolean]
113
+ def collect_payment?
114
+ payment_collect == "property" || payment_collect.nil?
115
+ end
116
+
117
+ # Whether payment was already collected by the OTA
118
+ # @return [Boolean]
119
+ def prepaid?
120
+ payment_collect == "ota"
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,68 @@
1
+ module Channex
2
+ module Objects
3
+ # Individual room within a booking revision
4
+ #
5
+ # @example
6
+ # room.room_type_id # => "uuid"
7
+ # room.check_in_date # => Date
8
+ # room.check_out_date # => Date
9
+ # room.nights # => 3
10
+ # room.occupancy.adults # => 2
11
+ # room.guests.first.name # => "John"
12
+ #
13
+ class BookingRoom < Channex::Object
14
+ attribute :booking_room_id
15
+ attribute :ota_unique_id
16
+ attribute :room_type_id
17
+ attribute :rate_plan_id
18
+ attribute :checkin_date
19
+ attribute :checkout_date
20
+ attribute :amount
21
+ attribute :occupancy, type: Occupancy
22
+ array_attribute :guests, type: Guest
23
+ array_attribute :taxes, type: Tax
24
+ array_attribute :services, type: Service
25
+
26
+ # Check-in date as Date object
27
+ # @return [Date, nil]
28
+ def check_in_date
29
+ Date.parse(checkin_date) if checkin_date
30
+ end
31
+
32
+ # Check-out date as Date object
33
+ # @return [Date, nil]
34
+ def check_out_date
35
+ Date.parse(checkout_date) if checkout_date
36
+ end
37
+
38
+ # Number of nights for this room
39
+ # @return [Integer, nil]
40
+ def nights
41
+ return nil unless check_in_date && check_out_date
42
+ (check_out_date - check_in_date).to_i
43
+ end
44
+
45
+ def room_reservation_id
46
+ meta["room_reservation_id"] || meta["roomreservation_id"]
47
+ end
48
+
49
+ # Daily price breakdown
50
+ # @return [Hash] Date string keys mapped to price strings
51
+ def days
52
+ @data["days"] || {}
53
+ end
54
+
55
+ # Free-form metadata
56
+ # @return [Hash]
57
+ def meta
58
+ @data["meta"] || {}
59
+ end
60
+
61
+ # Parent rate plan ID (when using derived rate plans)
62
+ # @return [String, nil]
63
+ def parent_rate_plan_id
64
+ meta["parent_rate_plan_id"]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,16 @@
1
+ module Channex
2
+ module Objects
3
+ # Company information nested within Customer
4
+ #
5
+ # @example
6
+ # company.title # => "Acme Corp"
7
+ # company.number # => "12345678"
8
+ # company.number_type # => "vat"
9
+ #
10
+ class Company < Channex::Object
11
+ attribute :title
12
+ attribute :number
13
+ attribute :number_type
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ module Channex
2
+ module Objects
3
+ # Customer information from a booking
4
+ #
5
+ # @example
6
+ # customer.name # => "John"
7
+ # customer.surname # => "Doe"
8
+ # customer.full_name # => "John Doe"
9
+ # customer.email # => "john@example.com"
10
+ # customer.company # => Channex::Objects::Company
11
+ #
12
+ class Customer < Channex::Object
13
+ attribute :name
14
+ attribute :surname
15
+ attribute :mail
16
+ attribute :phone
17
+ attribute :country
18
+ attribute :city
19
+ attribute :address
20
+ attribute :zip
21
+ attribute :language
22
+ attribute :company, type: Company
23
+
24
+ # Alias for mail (more intuitive name)
25
+ # @return [String, nil]
26
+ def email
27
+ mail
28
+ end
29
+
30
+ # Combined first and last name
31
+ # @return [String, nil]
32
+ def full_name
33
+ [name, surname].compact.join(" ").presence
34
+ end
35
+
36
+ # Free-form metadata
37
+ # @return [Hash]
38
+ def meta
39
+ @data["meta"] || {}
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ module Channex
2
+ module Objects
3
+ # Payment guarantee/card information
4
+ #
5
+ # Note: Contains sensitive payment data. Handle with care.
6
+ #
7
+ # @example
8
+ # guarantee.card_type # => "visa"
9
+ # guarantee.card_number # => "4111111111111111"
10
+ # guarantee.cardholder_name # => "John Doe"
11
+ # guarantee.expiration_date # => "12/25"
12
+ #
13
+ class Guarantee < Channex::Object
14
+ attribute :card_number
15
+ attribute :card_type
16
+ attribute :cardholder_name
17
+ attribute :cvv
18
+ attribute :expiration_date
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Channex
2
+ module Objects
3
+ # Individual guest information within a room
4
+ #
5
+ # @example
6
+ # guest.name # => "John"
7
+ # guest.surname # => "Doe"
8
+ # guest.full_name # => "John Doe"
9
+ #
10
+ class Guest < Channex::Object
11
+ attribute :name
12
+ attribute :surname
13
+
14
+ # Combined first and last name
15
+ # @return [String, nil]
16
+ def full_name
17
+ [name, surname].compact.join(" ").presence
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,95 @@
1
+ module Channex
2
+ module Objects
3
+ # A single message inside a Message Thread.
4
+ #
5
+ # The API returns messages as JSON:API resource nodes — the text lives under
6
+ # `attributes`, the identifier under `id` and the owning thread under
7
+ # `relationships`. {.from_resource} flattens that node so a message is one
8
+ # hash you can read, store and re-wrap.
9
+ #
10
+ # @example
11
+ # message.sender # => "guest"
12
+ # message.from_guest? # => true
13
+ # message.message # => "What time is check-in?"
14
+ # message.thread_id # => "4e160f0b-016a-424a-a30d-a9f0a8b1cbaa"
15
+ #
16
+ class Message < Channex::Object
17
+ # Who put the message on the thread. `system` marks messages Channex
18
+ # generates itself, such as an Airbnb inquiry.
19
+ SENDERS = %w[guest property system].freeze
20
+
21
+ attribute :id
22
+ attribute :message
23
+ attribute :sender
24
+ attribute :inserted_at
25
+ attribute :updated_at
26
+
27
+ # Lifted out of `relationships`
28
+ attribute :thread_id
29
+ attribute :user_id
30
+
31
+ # Flatten a JSON:API resource node into a message
32
+ #
33
+ # @param node [Hash, nil] A `{"id", "attributes", "relationships"}` node
34
+ # @return [Message, nil]
35
+ def self.from_resource(node)
36
+ return nil if node.nil?
37
+
38
+ attributes = node["attributes"] || {}
39
+ relationships = node["relationships"] || {}
40
+
41
+ new(
42
+ attributes.merge(
43
+ "id" => node["id"],
44
+ "thread_id" => relationships.dig("message_thread", "data", "id"),
45
+ "user_id" => relationships.dig("user", "data", "id")
46
+ ).compact
47
+ )
48
+ end
49
+
50
+ # Attachments as returned by the API. Channex sends relative URLs, which
51
+ # have to be prefixed with the API base URL to be fetched.
52
+ #
53
+ # @return [Array]
54
+ def attachments
55
+ @data["attachments"] || []
56
+ end
57
+
58
+ # Attachment URLs, whether the API sent bare strings or objects.
59
+ #
60
+ # @return [Array<String>]
61
+ def attachment_urls
62
+ attachments.filter_map do |attachment|
63
+ attachment.is_a?(Hash) ? attachment["url"] : attachment
64
+ end
65
+ end
66
+
67
+ # @return [Boolean]
68
+ def attachments?
69
+ !attachments.empty?
70
+ end
71
+
72
+ # Provider-specific extras, e.g. the booking details of an Airbnb inquiry.
73
+ #
74
+ # @return [Hash]
75
+ def meta
76
+ @data["meta"] || {}
77
+ end
78
+
79
+ # @return [Boolean]
80
+ def from_guest?
81
+ sender == "guest"
82
+ end
83
+
84
+ # @return [Boolean]
85
+ def from_property?
86
+ sender == "property"
87
+ end
88
+
89
+ # @return [Boolean]
90
+ def from_system?
91
+ sender == "system"
92
+ end
93
+ end
94
+ end
95
+ end