sendly 3.31.0 → 3.34.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 +4 -4
- data/CHANGELOG.md +57 -0
- data/Gemfile.lock +1 -1
- data/README.md +41 -0
- data/lib/sendly/business_upgrade_resource.rb +404 -0
- data/lib/sendly/client.rb +14 -0
- data/lib/sendly/conversations_resource.rb +8 -0
- data/lib/sendly/numbers_resource.rb +248 -0
- data/lib/sendly/types.rb +69 -0
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71c3ad356b4aa2903e05f1f174ce9a70f1fb1fe5cc4b0748ae69db333a0c25ac
|
|
4
|
+
data.tar.gz: 86df6410241d17dc84854057d19b2b13e6f32f504bf87243e276ec9260f78a34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 385c9b3f1973a51b923fed18d0b23a128c060d0d122ed782e08f3a21c1477ece2caca8aaeba65c71c8c494946630ffe21e9ac12eade63530ea207de10dfa0661
|
|
7
|
+
data.tar.gz: 6e274e7b40280d52118812b61ffa31b936d912baa0147027a448c7156d110ed24e4c3c0ba70f2a589bae3d0b3531316761a80fe2864fa821415002e0d3741f01
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# sendly (Ruby)
|
|
2
2
|
|
|
3
|
+
## 3.33.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- New `client.conversations.suggest_replies(id)` method — `POST /api/v1/conversations/:id/suggest-replies`. Returns AI-generated reply suggestions for a conversation based on its recent message history, mirroring the Node SDK's `conversations.suggestReplies()` and the equivalent methods on the other Sendly SDKs (closes a feature parity gap). Returns a `Sendly::SuggestRepliesResponse`, which is `Enumerable` over its `SuggestedReply` entries and also exposes `#suggestions`, `#based_on_message_id`, and `#model`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
client = Sendly::Client.new("sk_live_v1_xxx")
|
|
11
|
+
|
|
12
|
+
result = client.conversations.suggest_replies("conv_abc123")
|
|
13
|
+
result.suggestions.each do |reply|
|
|
14
|
+
puts "[#{reply.tone}] #{reply.text}"
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 3.32.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- New `business_upgrade` resource for the toll-free entity-upgrade ("fork-with-new-number") flow. When a customer forms a new legal entity (e.g. an LLC), this resource lets them reserve a new toll-free number under the new entity, submit it for carrier review, and atomically swap to it on approval — without disrupting outbound SMS during the 1-2 week review window. Mirrors the Node SDK's `businessUpgrade` resource at parity.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
client = Sendly::Client.new("sk_live_v1_xxx")
|
|
26
|
+
|
|
27
|
+
# Validate before submitting (no writes)
|
|
28
|
+
preview = client.business_upgrade.preflight(
|
|
29
|
+
business_name: "Acme Holdings LLC",
|
|
30
|
+
brn: "12-3456789",
|
|
31
|
+
brn_type: "EIN",
|
|
32
|
+
brn_country: "US",
|
|
33
|
+
entity_type: "PRIVATE_PROFIT"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Best-of prefill across all the caller's verified workspaces
|
|
37
|
+
prefill = client.business_upgrade.best_prefill
|
|
38
|
+
|
|
39
|
+
# Submit the upgrade with the IRS letter (multipart upload)
|
|
40
|
+
result = client.business_upgrade.start(
|
|
41
|
+
"ws_abc",
|
|
42
|
+
business_name: "Acme Holdings LLC",
|
|
43
|
+
brn: "12-3456789",
|
|
44
|
+
brn_type: "EIN",
|
|
45
|
+
brn_country: "US",
|
|
46
|
+
entity_type: "PRIVATE_PROFIT",
|
|
47
|
+
ein_doc_path: "./CP-575.pdf"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Status, cancel, resubmit, set old-number disposition
|
|
51
|
+
client.business_upgrade.status("ws_abc")
|
|
52
|
+
client.business_upgrade.cancel("ws_abc")
|
|
53
|
+
client.business_upgrade.resubmit("ws_abc", contact_email: "new@acme.com")
|
|
54
|
+
client.business_upgrade.set_disposition("ws_abc", disposition: "released")
|
|
55
|
+
client.business_upgrade.set_disposition("ws_abc", disposition: "moved", target_workspace_id: "ws_xyz")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Methods: `preflight`, `best_prefill`, `start`, `status`, `cancel`, `resubmit`, `set_disposition`. EIN PDFs can be passed via `ein_doc_path:` (file path) or `ein_doc:` (raw bytes / IO).
|
|
59
|
+
|
|
3
60
|
## 3.31.0
|
|
4
61
|
|
|
5
62
|
### Patch Changes
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -291,6 +291,47 @@ puts "New key: #{result['key']}" # Only shown once!
|
|
|
291
291
|
client.account.revoke_api_key('key_xxx')
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
+
## Numbers
|
|
295
|
+
|
|
296
|
+
Search for, list, and purchase phone numbers. Requires an API key with the
|
|
297
|
+
`numbers:read` / `numbers:write` scopes.
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
# List supported countries and the number types available in each
|
|
301
|
+
client.numbers.list_countries[:countries].each do |country|
|
|
302
|
+
puts "#{country.code} #{country.name}: #{country.number_types.join(', ')}"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Find available numbers (monthly_cost is already customer-priced)
|
|
306
|
+
result = client.numbers.list_available(country: 'GB', type: 'mobile', contains: '777')
|
|
307
|
+
number = result[:numbers].first
|
|
308
|
+
puts "#{number.phone_number} — #{number.monthly_cost} #{number.currency}/mo"
|
|
309
|
+
|
|
310
|
+
# List numbers you already own
|
|
311
|
+
client.numbers.list[:numbers].each do |n|
|
|
312
|
+
puts "#{n.phone_number} (#{n.status})"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Buy a number
|
|
316
|
+
purchase = client.numbers.buy(
|
|
317
|
+
phone_number: number.phone_number,
|
|
318
|
+
country_code: number.country,
|
|
319
|
+
phone_number_type: number.number_type,
|
|
320
|
+
monthly_cost: number.monthly_cost
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
case purchase.status
|
|
324
|
+
when 'provisioning'
|
|
325
|
+
puts "Provisioning #{purchase.number.phone_number}"
|
|
326
|
+
when 'documents_required', 'payment_required'
|
|
327
|
+
# Hand the user the hosted page + code, wait for them to finish, then
|
|
328
|
+
# re-call buy with the SAME arguments plus the completed action's code.
|
|
329
|
+
puts "Visit #{purchase.action_url} and enter code #{purchase.action_code}"
|
|
330
|
+
# ...after the action completes:
|
|
331
|
+
# client.numbers.buy(..., action_code: purchase.action_code)
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
294
335
|
## Error Handling
|
|
295
336
|
|
|
296
337
|
```ruby
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module Sendly
|
|
9
|
+
# Business Upgrade resource — Entity-Upgrade ("fork-with-new-number")
|
|
10
|
+
#
|
|
11
|
+
# Manages the toll-free business entity upgrade flow: when a customer
|
|
12
|
+
# forms a new legal entity (e.g. an LLC), this resource lets them
|
|
13
|
+
# reserve a new toll-free number under the new entity, submit it for
|
|
14
|
+
# carrier review, and atomically swap to it on approval — without
|
|
15
|
+
# disrupting outbound SMS during the 1-2 week review window.
|
|
16
|
+
#
|
|
17
|
+
# @see https://sendly.live/docs/business-upgrade
|
|
18
|
+
#
|
|
19
|
+
# @example Validate before submitting
|
|
20
|
+
# preview = client.business_upgrade.preflight(
|
|
21
|
+
# business_name: "Acme Holdings LLC",
|
|
22
|
+
# brn: "12-3456789",
|
|
23
|
+
# brn_type: "EIN",
|
|
24
|
+
# brn_country: "US",
|
|
25
|
+
# entity_type: "PRIVATE_PROFIT"
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# @example Submit an upgrade with the IRS letter
|
|
29
|
+
# result = client.business_upgrade.start(
|
|
30
|
+
# "ws_abc",
|
|
31
|
+
# business_name: "Acme Holdings LLC",
|
|
32
|
+
# brn: "12-3456789",
|
|
33
|
+
# brn_type: "EIN",
|
|
34
|
+
# brn_country: "US",
|
|
35
|
+
# entity_type: "PRIVATE_PROFIT",
|
|
36
|
+
# ein_doc_path: "./CP-575.pdf"
|
|
37
|
+
# )
|
|
38
|
+
class BusinessUpgradeResource
|
|
39
|
+
# Entity types accepted by the carrier
|
|
40
|
+
ENTITY_TYPES = %w[
|
|
41
|
+
SOLE_PROPRIETOR
|
|
42
|
+
PRIVATE_PROFIT
|
|
43
|
+
PUBLIC_PROFIT
|
|
44
|
+
NON_PROFIT
|
|
45
|
+
GOVERNMENT
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
# Business Registration Number types
|
|
49
|
+
BRN_TYPES = %w[EIN SSN DUNS CRA VAT LEI OTHER].freeze
|
|
50
|
+
|
|
51
|
+
# Disposition options for the old toll-free number after approval
|
|
52
|
+
DISPOSITIONS = %w[moved released].freeze
|
|
53
|
+
|
|
54
|
+
def initialize(client)
|
|
55
|
+
@client = client
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validate a candidate entity upgrade payload before submission.
|
|
59
|
+
# Returns issues + proposed auto-fixes. No writes — purely advisory.
|
|
60
|
+
#
|
|
61
|
+
# Accepts the same fields as +start+. Returns a hash with
|
|
62
|
+
# +verdict+ (+"ready"+, +"warnings"+, +"blocked"+), +issues+, and
|
|
63
|
+
# +proposedFixes+.
|
|
64
|
+
#
|
|
65
|
+
# @param business_name [String, nil]
|
|
66
|
+
# @param brn [String, nil]
|
|
67
|
+
# @param brn_type [String, nil] One of {BRN_TYPES}
|
|
68
|
+
# @param brn_country [String, nil] ISO country code
|
|
69
|
+
# @param entity_type [String, nil] One of {ENTITY_TYPES}
|
|
70
|
+
# @param doing_business_as [String, nil]
|
|
71
|
+
# @param website [String, nil]
|
|
72
|
+
# @param address1 [String, nil]
|
|
73
|
+
# @param address2 [String, nil]
|
|
74
|
+
# @param city [String, nil]
|
|
75
|
+
# @param state [String, nil]
|
|
76
|
+
# @param zip [String, nil]
|
|
77
|
+
# @param address_country [String, nil]
|
|
78
|
+
# @param contact_first_name [String, nil]
|
|
79
|
+
# @param contact_last_name [String, nil]
|
|
80
|
+
# @param contact_email [String, nil]
|
|
81
|
+
# @param contact_phone [String, nil]
|
|
82
|
+
# @param monthly_volume [String, nil]
|
|
83
|
+
# @param use_case [String, nil]
|
|
84
|
+
# @param use_case_summary [String, nil]
|
|
85
|
+
# @param sample_messages [String, nil]
|
|
86
|
+
# @param opt_in_workflow [String, nil]
|
|
87
|
+
# @param privacy_url [String, nil]
|
|
88
|
+
# @param terms_url [String, nil]
|
|
89
|
+
# @param additional_information [String, nil]
|
|
90
|
+
# @param age_gated_content [Boolean, nil]
|
|
91
|
+
# @return [Hash] Preflight report
|
|
92
|
+
def preflight(business_name: nil, brn: nil, brn_type: nil, brn_country: nil,
|
|
93
|
+
entity_type: nil, doing_business_as: nil, website: nil,
|
|
94
|
+
address1: nil, address2: nil, city: nil, state: nil, zip: nil,
|
|
95
|
+
address_country: nil, contact_first_name: nil,
|
|
96
|
+
contact_last_name: nil, contact_email: nil, contact_phone: nil,
|
|
97
|
+
monthly_volume: nil, use_case: nil, use_case_summary: nil,
|
|
98
|
+
sample_messages: nil, opt_in_workflow: nil, privacy_url: nil,
|
|
99
|
+
terms_url: nil, additional_information: nil,
|
|
100
|
+
age_gated_content: nil)
|
|
101
|
+
body = build_upgrade_body(
|
|
102
|
+
business_name: business_name, brn: brn, brn_type: brn_type,
|
|
103
|
+
brn_country: brn_country, entity_type: entity_type,
|
|
104
|
+
doing_business_as: doing_business_as, website: website,
|
|
105
|
+
address1: address1, address2: address2, city: city, state: state,
|
|
106
|
+
zip: zip, address_country: address_country,
|
|
107
|
+
contact_first_name: contact_first_name,
|
|
108
|
+
contact_last_name: contact_last_name, contact_email: contact_email,
|
|
109
|
+
contact_phone: contact_phone, monthly_volume: monthly_volume,
|
|
110
|
+
use_case: use_case, use_case_summary: use_case_summary,
|
|
111
|
+
sample_messages: sample_messages, opt_in_workflow: opt_in_workflow,
|
|
112
|
+
privacy_url: privacy_url, terms_url: terms_url,
|
|
113
|
+
additional_information: additional_information,
|
|
114
|
+
age_gated_content: age_gated_content
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@client.post("/verification/preflight", body)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get a "best-of" prefill across all the caller's verified workspaces.
|
|
121
|
+
# Returns most-recent non-empty values per messaging field. Use this
|
|
122
|
+
# to pre-populate the upgrade form for users whose current workspace
|
|
123
|
+
# has incomplete data.
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash] +{ "prefill" => {...}, "sourceWorkspaceCount" => Integer }+
|
|
126
|
+
def best_prefill
|
|
127
|
+
@client.get("/verification/best-prefill")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Start an entity upgrade for the given workspace. Auto-provisions
|
|
131
|
+
# a new toll-free number + messaging profile and submits to the
|
|
132
|
+
# carrier for review. Returns the pending verification details.
|
|
133
|
+
#
|
|
134
|
+
# The current toll-free number continues sending throughout the
|
|
135
|
+
# 1-2 week carrier review; on approval, an atomic swap promotes
|
|
136
|
+
# the new number.
|
|
137
|
+
#
|
|
138
|
+
# The EIN document (when supplied) is uploaded as multipart form-data
|
|
139
|
+
# under the +einDoc+ field. Provide either +ein_doc_path+ (a path on
|
|
140
|
+
# disk) or +ein_doc+ (raw bytes / IO) — not both.
|
|
141
|
+
#
|
|
142
|
+
# @param workspace_id [String]
|
|
143
|
+
# @param business_name [String]
|
|
144
|
+
# @param brn [String]
|
|
145
|
+
# @param brn_type [String] One of {BRN_TYPES}
|
|
146
|
+
# @param brn_country [String] ISO country code
|
|
147
|
+
# @param entity_type [String] One of {ENTITY_TYPES}
|
|
148
|
+
# @param ein_doc_path [String, nil] Path to the EIN/CP-575 PDF
|
|
149
|
+
# @param ein_doc [String, IO, nil] Raw EIN PDF bytes or IO
|
|
150
|
+
# @param ein_doc_filename [String, nil] Filename (defaults to "ein-doc.pdf")
|
|
151
|
+
# @param ein_doc_content_type [String, nil] MIME type (defaults to "application/pdf")
|
|
152
|
+
# @return [Hash] Upgrade start response
|
|
153
|
+
def start(workspace_id, business_name:, brn:, brn_type:, brn_country:,
|
|
154
|
+
entity_type:, doing_business_as: nil, website: nil,
|
|
155
|
+
address1: nil, address2: nil, city: nil, state: nil, zip: nil,
|
|
156
|
+
address_country: nil, contact_first_name: nil,
|
|
157
|
+
contact_last_name: nil, contact_email: nil, contact_phone: nil,
|
|
158
|
+
monthly_volume: nil, use_case: nil, use_case_summary: nil,
|
|
159
|
+
sample_messages: nil, opt_in_workflow: nil, privacy_url: nil,
|
|
160
|
+
terms_url: nil, additional_information: nil,
|
|
161
|
+
age_gated_content: nil, ein_doc_path: nil, ein_doc: nil,
|
|
162
|
+
ein_doc_filename: nil, ein_doc_content_type: nil)
|
|
163
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
164
|
+
|
|
165
|
+
fields = build_upgrade_body(
|
|
166
|
+
business_name: business_name, brn: brn, brn_type: brn_type,
|
|
167
|
+
brn_country: brn_country, entity_type: entity_type,
|
|
168
|
+
doing_business_as: doing_business_as, website: website,
|
|
169
|
+
address1: address1, address2: address2, city: city, state: state,
|
|
170
|
+
zip: zip, address_country: address_country,
|
|
171
|
+
contact_first_name: contact_first_name,
|
|
172
|
+
contact_last_name: contact_last_name, contact_email: contact_email,
|
|
173
|
+
contact_phone: contact_phone, monthly_volume: monthly_volume,
|
|
174
|
+
use_case: use_case, use_case_summary: use_case_summary,
|
|
175
|
+
sample_messages: sample_messages, opt_in_workflow: opt_in_workflow,
|
|
176
|
+
privacy_url: privacy_url, terms_url: terms_url,
|
|
177
|
+
additional_information: additional_information,
|
|
178
|
+
age_gated_content: age_gated_content
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
post_multipart_with_fields(
|
|
182
|
+
"/workspaces/#{url_escape(workspace_id)}/upgrade",
|
|
183
|
+
fields,
|
|
184
|
+
ein_doc_path: ein_doc_path,
|
|
185
|
+
ein_doc: ein_doc,
|
|
186
|
+
ein_doc_filename: ein_doc_filename,
|
|
187
|
+
ein_doc_content_type: ein_doc_content_type
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check whether the given workspace has a pending entity upgrade.
|
|
192
|
+
# Returns +{ "pending" => nil }+ when no upgrade is in flight.
|
|
193
|
+
#
|
|
194
|
+
# @param workspace_id [String]
|
|
195
|
+
# @return [Hash]
|
|
196
|
+
def status(workspace_id)
|
|
197
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
198
|
+
|
|
199
|
+
@client.get("/workspaces/#{url_escape(workspace_id)}/upgrade/status")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Cancel a pending entity upgrade for the given workspace. Releases
|
|
203
|
+
# the reserved toll-free number, deletes the new messaging profile,
|
|
204
|
+
# and removes the stored EIN document. Idempotent.
|
|
205
|
+
#
|
|
206
|
+
# @param workspace_id [String]
|
|
207
|
+
# @return [Hash]
|
|
208
|
+
def cancel(workspace_id)
|
|
209
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
210
|
+
|
|
211
|
+
@client.post("/workspaces/#{url_escape(workspace_id)}/upgrade/cancel", {})
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Resubmit a rejected (or waiting-for-customer) entity upgrade with
|
|
215
|
+
# updated fields and optionally a new EIN document. All fields are
|
|
216
|
+
# optional — only the ones you pass are sent.
|
|
217
|
+
#
|
|
218
|
+
# @param workspace_id [String]
|
|
219
|
+
# @return [Hash]
|
|
220
|
+
def resubmit(workspace_id, business_name: nil, brn: nil, brn_type: nil,
|
|
221
|
+
brn_country: nil, entity_type: nil, doing_business_as: nil,
|
|
222
|
+
website: nil, address1: nil, address2: nil, city: nil,
|
|
223
|
+
state: nil, zip: nil, address_country: nil,
|
|
224
|
+
contact_first_name: nil, contact_last_name: nil,
|
|
225
|
+
contact_email: nil, contact_phone: nil, monthly_volume: nil,
|
|
226
|
+
use_case: nil, use_case_summary: nil, sample_messages: nil,
|
|
227
|
+
opt_in_workflow: nil, privacy_url: nil, terms_url: nil,
|
|
228
|
+
additional_information: nil, age_gated_content: nil,
|
|
229
|
+
ein_doc_path: nil, ein_doc: nil, ein_doc_filename: nil,
|
|
230
|
+
ein_doc_content_type: nil)
|
|
231
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
232
|
+
|
|
233
|
+
fields = build_upgrade_body(
|
|
234
|
+
business_name: business_name, brn: brn, brn_type: brn_type,
|
|
235
|
+
brn_country: brn_country, entity_type: entity_type,
|
|
236
|
+
doing_business_as: doing_business_as, website: website,
|
|
237
|
+
address1: address1, address2: address2, city: city, state: state,
|
|
238
|
+
zip: zip, address_country: address_country,
|
|
239
|
+
contact_first_name: contact_first_name,
|
|
240
|
+
contact_last_name: contact_last_name, contact_email: contact_email,
|
|
241
|
+
contact_phone: contact_phone, monthly_volume: monthly_volume,
|
|
242
|
+
use_case: use_case, use_case_summary: use_case_summary,
|
|
243
|
+
sample_messages: sample_messages, opt_in_workflow: opt_in_workflow,
|
|
244
|
+
privacy_url: privacy_url, terms_url: terms_url,
|
|
245
|
+
additional_information: additional_information,
|
|
246
|
+
age_gated_content: age_gated_content
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
post_multipart_with_fields(
|
|
250
|
+
"/workspaces/#{url_escape(workspace_id)}/upgrade/resubmit",
|
|
251
|
+
fields,
|
|
252
|
+
ein_doc_path: ein_doc_path,
|
|
253
|
+
ein_doc: ein_doc,
|
|
254
|
+
ein_doc_filename: ein_doc_filename,
|
|
255
|
+
ein_doc_content_type: ein_doc_content_type
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# After a successful entity-upgrade approval, choose what happens to
|
|
260
|
+
# the old toll-free number:
|
|
261
|
+
#
|
|
262
|
+
# - +"moved"+: keep it active under another workspace owned by the
|
|
263
|
+
# same user (requires +target_workspace_id+)
|
|
264
|
+
# - +"released"+: return it to the carrier pool
|
|
265
|
+
#
|
|
266
|
+
# @param workspace_id [String]
|
|
267
|
+
# @param disposition [String] +"moved"+ or +"released"+
|
|
268
|
+
# @param target_workspace_id [String, nil] Required when +disposition+ is +"moved"+
|
|
269
|
+
# @return [Hash]
|
|
270
|
+
def set_disposition(workspace_id, disposition:, target_workspace_id: nil)
|
|
271
|
+
raise ArgumentError, "Workspace ID is required" if workspace_id.nil? || workspace_id.empty?
|
|
272
|
+
unless DISPOSITIONS.include?(disposition)
|
|
273
|
+
raise ArgumentError, "disposition must be one of: #{DISPOSITIONS.join(", ")}"
|
|
274
|
+
end
|
|
275
|
+
if disposition == "moved" && (target_workspace_id.nil? || target_workspace_id.empty?)
|
|
276
|
+
raise ArgumentError, "target_workspace_id is required when disposition is 'moved'"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
body = { disposition: disposition }
|
|
280
|
+
body[:targetOrgId] = target_workspace_id if target_workspace_id
|
|
281
|
+
|
|
282
|
+
@client.post("/workspaces/#{url_escape(workspace_id)}/upgrade/disposition", body)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def url_escape(value)
|
|
288
|
+
URI.encode_www_form_component(value)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def build_upgrade_body(business_name:, brn:, brn_type:, brn_country:,
|
|
292
|
+
entity_type:, doing_business_as:, website:,
|
|
293
|
+
address1:, address2:, city:, state:, zip:,
|
|
294
|
+
address_country:, contact_first_name:,
|
|
295
|
+
contact_last_name:, contact_email:, contact_phone:,
|
|
296
|
+
monthly_volume:, use_case:, use_case_summary:,
|
|
297
|
+
sample_messages:, opt_in_workflow:, privacy_url:,
|
|
298
|
+
terms_url:, additional_information:,
|
|
299
|
+
age_gated_content:)
|
|
300
|
+
body = {}
|
|
301
|
+
body[:businessName] = business_name unless business_name.nil?
|
|
302
|
+
body[:brn] = brn unless brn.nil?
|
|
303
|
+
body[:brnType] = brn_type unless brn_type.nil?
|
|
304
|
+
body[:brnCountry] = brn_country unless brn_country.nil?
|
|
305
|
+
body[:entityType] = entity_type unless entity_type.nil?
|
|
306
|
+
body[:doingBusinessAs] = doing_business_as unless doing_business_as.nil?
|
|
307
|
+
body[:website] = website unless website.nil?
|
|
308
|
+
body[:address1] = address1 unless address1.nil?
|
|
309
|
+
body[:address2] = address2 unless address2.nil?
|
|
310
|
+
body[:city] = city unless city.nil?
|
|
311
|
+
body[:state] = state unless state.nil?
|
|
312
|
+
body[:zip] = zip unless zip.nil?
|
|
313
|
+
body[:addressCountry] = address_country unless address_country.nil?
|
|
314
|
+
body[:contactFirstName] = contact_first_name unless contact_first_name.nil?
|
|
315
|
+
body[:contactLastName] = contact_last_name unless contact_last_name.nil?
|
|
316
|
+
body[:contactEmail] = contact_email unless contact_email.nil?
|
|
317
|
+
body[:contactPhone] = contact_phone unless contact_phone.nil?
|
|
318
|
+
body[:monthlyVolume] = monthly_volume unless monthly_volume.nil?
|
|
319
|
+
body[:useCase] = use_case unless use_case.nil?
|
|
320
|
+
body[:useCaseSummary] = use_case_summary unless use_case_summary.nil?
|
|
321
|
+
body[:sampleMessages] = sample_messages unless sample_messages.nil?
|
|
322
|
+
body[:optInWorkflow] = opt_in_workflow unless opt_in_workflow.nil?
|
|
323
|
+
body[:privacyUrl] = privacy_url unless privacy_url.nil?
|
|
324
|
+
body[:termsUrl] = terms_url unless terms_url.nil?
|
|
325
|
+
body[:additionalInformation] = additional_information unless additional_information.nil?
|
|
326
|
+
body[:ageGatedContent] = age_gated_content unless age_gated_content.nil?
|
|
327
|
+
body
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# POST a multipart/form-data request with the given fields plus an
|
|
331
|
+
# optional +einDoc+ file part. Mirrors the JSON-vs-multipart flexibility
|
|
332
|
+
# the server expects: if no EIN doc is supplied and there are no fields,
|
|
333
|
+
# we still send an empty multipart body (the server is configured for
|
|
334
|
+
# +multer+ on these routes).
|
|
335
|
+
def post_multipart_with_fields(path, fields, ein_doc_path:, ein_doc:,
|
|
336
|
+
ein_doc_filename:, ein_doc_content_type:)
|
|
337
|
+
if !ein_doc_path.nil? && !ein_doc.nil?
|
|
338
|
+
raise ArgumentError, "Provide ein_doc_path OR ein_doc, not both"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
file_bytes = nil
|
|
342
|
+
filename = ein_doc_filename || "ein-doc.pdf"
|
|
343
|
+
content_type = ein_doc_content_type || "application/pdf"
|
|
344
|
+
|
|
345
|
+
if ein_doc_path
|
|
346
|
+
raise ArgumentError, "File not found: #{ein_doc_path}" unless File.exist?(ein_doc_path)
|
|
347
|
+
file_bytes = File.binread(ein_doc_path)
|
|
348
|
+
filename = ein_doc_filename || File.basename(ein_doc_path)
|
|
349
|
+
elsif ein_doc
|
|
350
|
+
file_bytes = ein_doc.is_a?(String) ? ein_doc : ein_doc.read
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
boundary = "SendlyRuby#{SecureRandom.hex(16)}"
|
|
354
|
+
body_parts = []
|
|
355
|
+
|
|
356
|
+
fields.each do |k, v|
|
|
357
|
+
next if v.nil?
|
|
358
|
+
body_parts << "--#{boundary}\r\n"
|
|
359
|
+
body_parts << "Content-Disposition: form-data; name=\"#{k}\"\r\n\r\n"
|
|
360
|
+
body_parts << (v == true || v == false ? v.to_s : v.to_s)
|
|
361
|
+
body_parts << "\r\n"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
if file_bytes
|
|
365
|
+
body_parts << "--#{boundary}\r\n"
|
|
366
|
+
body_parts << "Content-Disposition: form-data; name=\"einDoc\"; filename=\"#{filename}\"\r\n"
|
|
367
|
+
body_parts << "Content-Type: #{content_type}\r\n\r\n"
|
|
368
|
+
body_parts << file_bytes
|
|
369
|
+
body_parts << "\r\n"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
body_parts << "--#{boundary}--\r\n"
|
|
373
|
+
|
|
374
|
+
uri = URI.parse("#{@client.base_url}#{path}")
|
|
375
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
376
|
+
http.use_ssl = uri.scheme == "https"
|
|
377
|
+
http.open_timeout = 10
|
|
378
|
+
http.read_timeout = @client.timeout
|
|
379
|
+
|
|
380
|
+
req = Net::HTTP::Post.new(uri)
|
|
381
|
+
req["Authorization"] = "Bearer #{@client.api_key}"
|
|
382
|
+
req["Accept"] = "application/json"
|
|
383
|
+
req["User-Agent"] = "sendly-ruby/#{Sendly::VERSION}"
|
|
384
|
+
req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
|
385
|
+
req["X-Organization-Id"] = @client.organization_id if @client.organization_id
|
|
386
|
+
req.body = body_parts.join
|
|
387
|
+
|
|
388
|
+
begin
|
|
389
|
+
response = http.request(req)
|
|
390
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
391
|
+
raise Sendly::TimeoutError, "Request timed out after #{@client.timeout} seconds"
|
|
392
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError => e
|
|
393
|
+
raise Sendly::NetworkError, "Connection failed: #{e.message}"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
status = response.code.to_i
|
|
397
|
+
body = response.body.nil? || response.body.empty? ? {} : (JSON.parse(response.body) rescue { "message" => response.body })
|
|
398
|
+
|
|
399
|
+
return body if status >= 200 && status < 300
|
|
400
|
+
|
|
401
|
+
raise Sendly::ErrorFactory.from_response(status, body)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
data/lib/sendly/client.rb
CHANGED
|
@@ -150,6 +150,20 @@ module Sendly
|
|
|
150
150
|
@enterprise ||= EnterpriseResource.new(self)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
+
# Access the Business Upgrade resource (entity-upgrade flow)
|
|
154
|
+
#
|
|
155
|
+
# @return [Sendly::BusinessUpgradeResource]
|
|
156
|
+
def business_upgrade
|
|
157
|
+
@business_upgrade ||= BusinessUpgradeResource.new(self)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Access the Numbers resource
|
|
161
|
+
#
|
|
162
|
+
# @return [Sendly::NumbersResource]
|
|
163
|
+
def numbers
|
|
164
|
+
@numbers ||= NumbersResource.new(self)
|
|
165
|
+
end
|
|
166
|
+
|
|
153
167
|
# Make a GET request
|
|
154
168
|
#
|
|
155
169
|
# @param path [String] API path
|
|
@@ -107,6 +107,14 @@ module Sendly
|
|
|
107
107
|
ConversationContext.new(response)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
def suggest_replies(id)
|
|
111
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
112
|
+
|
|
113
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
114
|
+
response = @client.post("/conversations/#{encoded_id}/suggest-replies")
|
|
115
|
+
SuggestRepliesResponse.new(response)
|
|
116
|
+
end
|
|
117
|
+
|
|
110
118
|
def each(status: nil, batch_size: 100, &block)
|
|
111
119
|
return enum_for(:each, status: status, batch_size: batch_size) unless block_given?
|
|
112
120
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
# A country in which numbers can be searched and purchased, along with the
|
|
5
|
+
# number types available there (e.g. "mobile", "local", "toll_free").
|
|
6
|
+
class NumberCountry
|
|
7
|
+
attr_reader :code, :name, :number_types
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@code = data["code"]
|
|
11
|
+
@name = data["name"]
|
|
12
|
+
@number_types = data["numberTypes"] || data["number_types"] || []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{ code: code, name: name, number_types: number_types }.compact
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A number that is available to purchase. The monthly cost is already
|
|
21
|
+
# customer-priced and returned as a string in the given currency.
|
|
22
|
+
class AvailableNumber
|
|
23
|
+
attr_reader :phone_number, :country, :number_type, :monthly_cost, :currency
|
|
24
|
+
|
|
25
|
+
def initialize(data)
|
|
26
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
27
|
+
@country = data["country"]
|
|
28
|
+
@number_type = data["numberType"] || data["number_type"]
|
|
29
|
+
@monthly_cost = data["monthlyCost"] || data["monthly_cost"]
|
|
30
|
+
@currency = data["currency"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
phone_number: phone_number, country: country,
|
|
36
|
+
number_type: number_type, monthly_cost: monthly_cost,
|
|
37
|
+
currency: currency
|
|
38
|
+
}.compact
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A number owned by the account.
|
|
43
|
+
class PhoneNumber
|
|
44
|
+
attr_reader :id, :phone_number, :status, :source, :country_code,
|
|
45
|
+
:phone_number_type, :monthly_cost_cents
|
|
46
|
+
|
|
47
|
+
def initialize(data)
|
|
48
|
+
@id = data["id"]
|
|
49
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
50
|
+
@status = data["status"]
|
|
51
|
+
@source = data["source"]
|
|
52
|
+
@country_code = data["countryCode"] || data["country_code"]
|
|
53
|
+
@phone_number_type = data["phoneNumberType"] || data["phone_number_type"]
|
|
54
|
+
@monthly_cost_cents = data["monthlyCostCents"] || data["monthly_cost_cents"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{
|
|
59
|
+
id: id, phone_number: phone_number, status: status, source: source,
|
|
60
|
+
country_code: country_code, phone_number_type: phone_number_type,
|
|
61
|
+
monthly_cost_cents: monthly_cost_cents
|
|
62
|
+
}.compact
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The result of a buy request. The API responds 202 with one of three
|
|
67
|
+
# statuses:
|
|
68
|
+
#
|
|
69
|
+
# - +"provisioning"+: the purchase succeeded and the number is being
|
|
70
|
+
# provisioned. +number+ carries the new {PhoneNumber}.
|
|
71
|
+
# - +"documents_required"+ / +"payment_required"+: the purchase is paused
|
|
72
|
+
# pending a hosted Sendly step. +action+ carries the hand-off object,
|
|
73
|
+
# which holds TWO distinct identifiers:
|
|
74
|
+
# - +actionCode+ — a 32-hex action identifier (read via {#action_identifier}).
|
|
75
|
+
# Use THIS to poll the action and to re-call +buy+ (pass it as the
|
|
76
|
+
# +action_code:+ argument).
|
|
77
|
+
# - +code+ — a short user code (read via {#action_code}). DISPLAY ONLY:
|
|
78
|
+
# show it to the human to type on the hosted page to prove terminal
|
|
79
|
+
# access. Never pass it back as +action_code:+.
|
|
80
|
+
# Hand the user +action_url+ + {#action_code}, wait for completion, then
|
|
81
|
+
# re-call +buy+ with the same body plus +action_code:+ set to
|
|
82
|
+
# {#action_identifier}. +requirements+ describes what's missing (a JSON
|
|
83
|
+
# array).
|
|
84
|
+
#
|
|
85
|
+
# The raw parsed response is preserved verbatim on +#raw+ so callers can
|
|
86
|
+
# read any field the server adds.
|
|
87
|
+
class NumberPurchase
|
|
88
|
+
attr_reader :status, :number, :requirements, :action, :raw
|
|
89
|
+
|
|
90
|
+
def initialize(data)
|
|
91
|
+
@raw = data
|
|
92
|
+
@status = data["status"]
|
|
93
|
+
@number = data["number"] ? PhoneNumber.new(data["number"]) : nil
|
|
94
|
+
@requirements = data["requirements"]
|
|
95
|
+
@action = data["action"]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def provisioning?
|
|
99
|
+
status == "provisioning"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def documents_required?
|
|
103
|
+
status == "documents_required"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def payment_required?
|
|
107
|
+
status == "payment_required"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [String, nil] The hosted Sendly page URL the user must visit.
|
|
111
|
+
def action_url
|
|
112
|
+
action && (action["url"] || action[:url])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The 32-hex action identifier. Use THIS to poll the action's status and
|
|
116
|
+
# to re-call +buy+ (pass it as the +action_code:+ argument). NOT for
|
|
117
|
+
# display.
|
|
118
|
+
#
|
|
119
|
+
# @return [String, nil]
|
|
120
|
+
def action_identifier
|
|
121
|
+
action && (action["actionCode"] || action["action_code"] || action[:actionCode] || action[:action_code])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The short user code shown to the human to type on the hosted page to
|
|
125
|
+
# prove terminal access. DISPLAY ONLY — to re-buy/poll, use
|
|
126
|
+
# {#action_identifier}, not this.
|
|
127
|
+
#
|
|
128
|
+
# @return [String, nil]
|
|
129
|
+
def action_code
|
|
130
|
+
action && (action["code"] || action[:code])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Expiry of the action, as an epoch-milliseconds number (the server sends
|
|
134
|
+
# a number, not an ISO-8601 string). Older payloads may carry it under
|
|
135
|
+
# +expires_at+; both are accepted.
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer, String, nil]
|
|
138
|
+
def action_expires_at
|
|
139
|
+
action && (action["expiresAt"] || action["expires_at"] || action[:expiresAt] || action[:expires_at])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def to_h
|
|
143
|
+
{
|
|
144
|
+
status: status, number: number&.to_h,
|
|
145
|
+
requirements: requirements, action: action
|
|
146
|
+
}.compact
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Numbers resource — search, list, and purchase phone numbers.
|
|
151
|
+
#
|
|
152
|
+
# @example List supported countries
|
|
153
|
+
# result = client.numbers.list_countries
|
|
154
|
+
# result[:countries].each { |c| puts "#{c.code} #{c.name}" }
|
|
155
|
+
#
|
|
156
|
+
# @example Find available mobile numbers in the UK
|
|
157
|
+
# result = client.numbers.list_available(country: "GB", type: "mobile")
|
|
158
|
+
# number = result[:numbers].first
|
|
159
|
+
#
|
|
160
|
+
# @example Buy a number (may pause for a hosted step)
|
|
161
|
+
# purchase = client.numbers.buy(
|
|
162
|
+
# phone_number: number.phone_number,
|
|
163
|
+
# country_code: number.country,
|
|
164
|
+
# phone_number_type: number.number_type,
|
|
165
|
+
# monthly_cost: number.monthly_cost
|
|
166
|
+
# )
|
|
167
|
+
# if purchase.documents_required? || purchase.payment_required?
|
|
168
|
+
# # Show the user the URL + display code; keep the 32-hex identifier for re-buy
|
|
169
|
+
# puts "Visit #{purchase.action_url} and enter code #{purchase.action_code}"
|
|
170
|
+
# # ...once they finish, re-call buy with action_code: purchase.action_identifier
|
|
171
|
+
# end
|
|
172
|
+
class NumbersResource
|
|
173
|
+
def initialize(client)
|
|
174
|
+
@client = client
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# List the countries in which numbers can be searched and purchased,
|
|
178
|
+
# along with the number types available in each.
|
|
179
|
+
#
|
|
180
|
+
# @return [Hash] +{ countries: Array<NumberCountry> }+
|
|
181
|
+
def list_countries
|
|
182
|
+
response = @client.get("/numbers/countries")
|
|
183
|
+
countries = (response["countries"] || []).map { |c| NumberCountry.new(c) }
|
|
184
|
+
{ countries: countries }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Search for numbers available to purchase in a country.
|
|
188
|
+
#
|
|
189
|
+
# @param country [String] ISO country code (e.g. "GB")
|
|
190
|
+
# @param type [String] Number type (e.g. "mobile", "local", "toll_free")
|
|
191
|
+
# @param contains [String, nil] Optional digit/letter filter
|
|
192
|
+
# @return [Hash] +{ numbers: Array<AvailableNumber> }+
|
|
193
|
+
def list_available(country:, type:, contains: nil)
|
|
194
|
+
raise ValidationError, "country is required" if country.nil? || country.to_s.empty?
|
|
195
|
+
raise ValidationError, "type is required" if type.nil? || type.to_s.empty?
|
|
196
|
+
|
|
197
|
+
params = { country: country, type: type }
|
|
198
|
+
params[:contains] = contains if contains
|
|
199
|
+
|
|
200
|
+
response = @client.get("/numbers/available", params)
|
|
201
|
+
numbers = (response["numbers"] || []).map { |n| AvailableNumber.new(n) }
|
|
202
|
+
{ numbers: numbers }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# List the numbers owned by the account.
|
|
206
|
+
#
|
|
207
|
+
# @return [Hash] +{ numbers: Array<PhoneNumber> }+
|
|
208
|
+
def list
|
|
209
|
+
response = @client.get("/numbers")
|
|
210
|
+
numbers = (response["numbers"] || []).map { |n| PhoneNumber.new(n) }
|
|
211
|
+
{ numbers: numbers }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Buy a number.
|
|
215
|
+
#
|
|
216
|
+
# Returns a {NumberPurchase}. When its status is +documents_required+ or
|
|
217
|
+
# +payment_required+, hand the user +purchase.action_url+ +
|
|
218
|
+
# +purchase.action_code+ (the short display code), wait for that hosted
|
|
219
|
+
# step to complete, then re-call +buy+ with the SAME arguments plus
|
|
220
|
+
# +action_code:+ set to +purchase.action_identifier+ (the 32-hex action
|
|
221
|
+
# identifier) — NOT the display code.
|
|
222
|
+
#
|
|
223
|
+
# @param phone_number [String]
|
|
224
|
+
# @param country_code [String]
|
|
225
|
+
# @param phone_number_type [String]
|
|
226
|
+
# @param monthly_cost [String] Customer-priced monthly cost (as returned by {#list_available})
|
|
227
|
+
# @param action_code [String, nil] The 32-hex action identifier of a
|
|
228
|
+
# completed hosted action (see {NumberPurchase#action_identifier}), on re-call
|
|
229
|
+
# @return [NumberPurchase]
|
|
230
|
+
def buy(phone_number:, country_code:, phone_number_type:, monthly_cost:, action_code: nil)
|
|
231
|
+
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
232
|
+
raise ValidationError, "country_code is required" if country_code.nil? || country_code.to_s.empty?
|
|
233
|
+
raise ValidationError, "phone_number_type is required" if phone_number_type.nil? || phone_number_type.to_s.empty?
|
|
234
|
+
raise ValidationError, "monthly_cost is required" if monthly_cost.nil? || monthly_cost.to_s.empty?
|
|
235
|
+
|
|
236
|
+
body = {
|
|
237
|
+
phoneNumber: phone_number,
|
|
238
|
+
countryCode: country_code,
|
|
239
|
+
phoneNumberType: phone_number_type,
|
|
240
|
+
monthlyCost: monthly_cost
|
|
241
|
+
}
|
|
242
|
+
body[:actionCode] = action_code if action_code
|
|
243
|
+
|
|
244
|
+
response = @client.post("/numbers/buy", body)
|
|
245
|
+
NumberPurchase.new(response)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
data/lib/sendly/types.rb
CHANGED
|
@@ -596,6 +596,75 @@ module Sendly
|
|
|
596
596
|
end
|
|
597
597
|
end
|
|
598
598
|
|
|
599
|
+
# ============================================================================
|
|
600
|
+
# Suggested Replies
|
|
601
|
+
# ============================================================================
|
|
602
|
+
|
|
603
|
+
class SuggestedReply
|
|
604
|
+
# @return [String] Suggested reply text
|
|
605
|
+
attr_reader :text
|
|
606
|
+
|
|
607
|
+
# @return [String] Tone of the suggestion (professional, friendly, concise)
|
|
608
|
+
attr_reader :tone
|
|
609
|
+
|
|
610
|
+
TONES = %w[professional friendly concise].freeze
|
|
611
|
+
|
|
612
|
+
def initialize(data)
|
|
613
|
+
@text = data["text"]
|
|
614
|
+
@tone = data["tone"]
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def to_h
|
|
618
|
+
{ text: text, tone: tone }.compact
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
class SuggestRepliesResponse
|
|
623
|
+
include Enumerable
|
|
624
|
+
|
|
625
|
+
# @return [Array<SuggestedReply>] AI-generated reply suggestions
|
|
626
|
+
attr_reader :suggestions
|
|
627
|
+
|
|
628
|
+
# @return [String, nil] ID of the inbound message the suggestions are based on
|
|
629
|
+
attr_reader :based_on_message_id
|
|
630
|
+
|
|
631
|
+
# @return [String, nil] Model that generated the suggestions
|
|
632
|
+
attr_reader :model
|
|
633
|
+
|
|
634
|
+
def initialize(data)
|
|
635
|
+
@suggestions = (data["suggestions"] || []).map { |s| SuggestedReply.new(s) }
|
|
636
|
+
@based_on_message_id = data["basedOnMessageId"] || data["based_on_message_id"]
|
|
637
|
+
@model = data["model"]
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def each(&block)
|
|
641
|
+
suggestions.each(&block)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def count
|
|
645
|
+
suggestions.length
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
alias size count
|
|
649
|
+
alias length count
|
|
650
|
+
|
|
651
|
+
def empty?
|
|
652
|
+
suggestions.empty?
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def first
|
|
656
|
+
suggestions.first
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def to_h
|
|
660
|
+
{
|
|
661
|
+
suggestions: suggestions.map(&:to_h),
|
|
662
|
+
based_on_message_id: based_on_message_id,
|
|
663
|
+
model: model
|
|
664
|
+
}.compact
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
599
668
|
# ============================================================================
|
|
600
669
|
# Labels
|
|
601
670
|
# ============================================================================
|
data/lib/sendly/version.rb
CHANGED
data/lib/sendly.rb
CHANGED
|
@@ -21,6 +21,8 @@ require_relative "sendly/labels_resource"
|
|
|
21
21
|
require_relative "sendly/drafts_resource"
|
|
22
22
|
require_relative "sendly/rules_resource"
|
|
23
23
|
require_relative "sendly/enterprise"
|
|
24
|
+
require_relative "sendly/business_upgrade_resource"
|
|
25
|
+
require_relative "sendly/numbers_resource"
|
|
24
26
|
|
|
25
27
|
# Sendly Ruby SDK
|
|
26
28
|
#
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sendly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.34.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sendly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- examples/send_sms.rb
|
|
126
126
|
- lib/sendly.rb
|
|
127
127
|
- lib/sendly/account_resource.rb
|
|
128
|
+
- lib/sendly/business_upgrade_resource.rb
|
|
128
129
|
- lib/sendly/campaigns_resource.rb
|
|
129
130
|
- lib/sendly/client.rb
|
|
130
131
|
- lib/sendly/contacts_resource.rb
|
|
@@ -135,6 +136,7 @@ files:
|
|
|
135
136
|
- lib/sendly/labels_resource.rb
|
|
136
137
|
- lib/sendly/media.rb
|
|
137
138
|
- lib/sendly/messages.rb
|
|
139
|
+
- lib/sendly/numbers_resource.rb
|
|
138
140
|
- lib/sendly/rules_resource.rb
|
|
139
141
|
- lib/sendly/templates_resource.rb
|
|
140
142
|
- lib/sendly/types.rb
|