sendly 4.0.0 → 4.2.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.
data/lib/sendly/client.rb CHANGED
@@ -192,6 +192,24 @@ module Sendly
192
192
  @rcs ||= RcsResource.new(self)
193
193
  end
194
194
 
195
+ # Access the Calls resource (phone calls handled by your AI agents)
196
+ #
197
+ # @return [Sendly::CallsResource]
198
+ def calls
199
+ @calls ||= CallsResource.new(self)
200
+ end
201
+
202
+ # Access the Voice resource (numbers, AI agents and voices for phone calls)
203
+ #
204
+ # @return [Sendly::VoiceResource]
205
+ #
206
+ # @example
207
+ # agent = client.voice.agents.create(name: "Front desk")
208
+ # client.voice.numbers.update("+15555550188", voice_enabled: true, voice_mode: "agent", agent_id: agent.id)
209
+ def voice
210
+ @voice ||= VoiceResource.new(self)
211
+ end
212
+
195
213
  # Make a GET request
196
214
  #
197
215
  # @param path [String] API path
@@ -248,10 +266,14 @@ module Sendly
248
266
 
249
267
  # Make a DELETE request
250
268
  #
269
+ # No Idempotency-Key is generated for a DELETE; pass +idempotency_key+
270
+ # to send one (1-255 printable ASCII characters).
271
+ #
251
272
  # @param path [String] API path
273
+ # @param idempotency_key [String, nil] Caller-supplied idempotency key (optional)
252
274
  # @return [Hash] Response body
253
- def delete(path)
254
- request(:delete, path)
275
+ def delete(path, idempotency_key: nil)
276
+ request(:delete, path, idempotency_key: idempotency_key)
255
277
  end
256
278
 
257
279
  # Make a GET request against the API origin, bypassing the +/api/v1+ base.
data/lib/sendly/errors.rb CHANGED
@@ -12,12 +12,30 @@ module Sendly
12
12
  # @return [Integer, nil] HTTP status code
13
13
  attr_reader :status_code
14
14
 
15
+ # @return [Hash, nil] The parsed JSON body of the API's error response,
16
+ # or +nil+ for errors raised before a request was sent. The API's own
17
+ # error code is under +"error"+, and some refusals carry more than a
18
+ # message: a 409 +agent_in_use+ lists the numbers the agent still
19
+ # answers under +"numbers"+, and a 422 +invalid_address+ carries a
20
+ # corrected address (or +nil+) under +"suggested"+.
21
+ attr_reader :response_body
22
+
15
23
  def initialize(message = nil, code: nil, details: nil, status_code: nil)
16
24
  @code = code
17
25
  @details = details
18
26
  @status_code = status_code
19
27
  super(message)
20
28
  end
29
+
30
+ # Attach the parsed body of the API response this error came from.
31
+ #
32
+ # @api private
33
+ # @param body [Hash, nil]
34
+ # @return [self]
35
+ def with_response_body(body)
36
+ @response_body = body
37
+ self
38
+ end
21
39
  end
22
40
 
23
41
  # Raised when the API key is invalid or missing
@@ -101,23 +119,24 @@ module Sendly
101
119
  code = body["code"]
102
120
  details = body["details"]
103
121
 
104
- case status
105
- when 400, 422
106
- ValidationError.new(message, details: details, field_errors: body["errors"])
107
- when 401
108
- AuthenticationError.new(message)
109
- when 402
110
- InsufficientCreditsError.new(message)
111
- when 404
112
- NotFoundError.new(message)
113
- when 429
114
- retry_after = body["retry_after"] || body["retryAfter"]
115
- RateLimitError.new(message, retry_after: retry_after)
116
- when 500..599
117
- ServerError.new(message, status_code: status)
118
- else
119
- APIError.new(message, status_code: status, code: code, details: details)
120
- end
122
+ error = case status
123
+ when 400, 422
124
+ ValidationError.new(message, details: details, field_errors: body["errors"])
125
+ when 401
126
+ AuthenticationError.new(message)
127
+ when 402
128
+ InsufficientCreditsError.new(message)
129
+ when 404
130
+ NotFoundError.new(message)
131
+ when 429
132
+ retry_after = body["retry_after"] || body["retryAfter"]
133
+ RateLimitError.new(message, retry_after: retry_after)
134
+ when 500..599
135
+ ServerError.new(message, status_code: status)
136
+ else
137
+ APIError.new(message, status_code: status, code: code, details: details)
138
+ end
139
+ error.with_response_body(body)
121
140
  end
122
141
  end
123
142
  end
@@ -53,9 +53,22 @@ module Sendly
53
53
  # true when the number is scheduled for release at period end.
54
54
  :pending_cancellation,
55
55
  # ISO-8601 timestamp string, or nil when no release is scheduled.
56
- :scheduled_release_at
56
+ :scheduled_release_at,
57
+ # true when the number can place and receive phone calls
58
+ # (switched on with {VoiceNumbersResource#update} or in the
59
+ # dashboard under Calls, then Settings).
60
+ :voice_enabled,
61
+ # One of {VOICE_MODES}: "none" when voice is off, "ring_dashboard"
62
+ # when inbound calls ring the team, "agent" when an AI agent answers.
63
+ :voice_mode
64
+
65
+ # @return [Hash] The raw parsed response
66
+ attr_reader :raw
67
+
68
+ VOICE_MODES = %w[none ring_dashboard agent].freeze
57
69
 
58
70
  def initialize(data)
71
+ @raw = data
59
72
  @id = data["id"]
60
73
  @phone_number = data["phoneNumber"] || data["phone_number"]
61
74
  @status = data["status"]
@@ -67,6 +80,13 @@ module Sendly
67
80
  @requirements_submitted_at = data["requirementsSubmittedAt"] || data["requirements_submitted_at"]
68
81
  @pending_cancellation = data.key?("pendingCancellation") ? data["pendingCancellation"] : data["pending_cancellation"]
69
82
  @scheduled_release_at = data["scheduledReleaseAt"] || data["scheduled_release_at"]
83
+ @voice_enabled = data.key?("voiceEnabled") ? data["voiceEnabled"] : data["voice_enabled"]
84
+ @voice_mode = data["voiceMode"] || data["voice_mode"]
85
+ end
86
+
87
+ # @return [Boolean] Whether the number can be used as the +from+ of a call
88
+ def voice_enabled?
89
+ voice_enabled == true
70
90
  end
71
91
 
72
92
  def to_h
@@ -76,7 +96,8 @@ module Sendly
76
96
  monthly_cost_cents: monthly_cost_cents, is_default: is_default,
77
97
  requirements_submitted_at: requirements_submitted_at,
78
98
  pending_cancellation: pending_cancellation,
79
- scheduled_release_at: scheduled_release_at
99
+ scheduled_release_at: scheduled_release_at,
100
+ voice_enabled: voice_enabled, voice_mode: voice_mode
80
101
  }.compact
81
102
  end
82
103
  end
@@ -222,10 +222,6 @@ module Sendly
222
222
  end
223
223
 
224
224
  # Copy an existing template into a new draft.
225
- #
226
- # @note Not available yet: the versioned API serves no clone route, so
227
- # this call fails with a 404. To copy a template today, read it with
228
- # {#get} and pass its {Template#text} to {#create}.
229
225
  def clone(id, name: nil)
230
226
  body = {}
231
227
  body[:name] = name if name
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "4.0.0"
4
+ VERSION = "4.2.0"
5
5
  end