sendly 3.38.0 → 3.39.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 +22 -0
- data/Gemfile.lock +1 -1
- data/README.md +128 -3
- data/lib/sendly/client.rb +12 -4
- data/lib/sendly/errors.rb +4 -2
- data/lib/sendly/rcs_resource.rb +944 -11
- data/lib/sendly/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c474ade97511ceecadbff15268b602edb67953395417996e9c1ac8b7626e68ff
|
|
4
|
+
data.tar.gz: b034a8b316eab9f85e98de49e31db3045326662beeefbafb306844302a147037
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10f0a1eecb587b5c0b801c1abb5366d41caaa4fecf7e274ac3a37860d82bbdf92ca644280a419626391d0fce53679c35172a66aca3f26893d780bb499321c15c
|
|
7
|
+
data.tar.gz: 0221a6cfd5de361ff4648b4b4e0256ceccf4bab50e3b44b2616cd2200c136c2acd27409c3b20a99f2f81176663e0e272664a07d07f113d29d2b2064a0546bd33
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# sendly (Ruby)
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **`Sendly::ValidationError#field_errors` is now populated.** It was always `nil` before, because the API path never passed it. It now carries the response body's `errors` array on any 400 or 422, on every resource rather than just RCS: `client.contacts.import` already returns one, for example. Each entry is a Hash. Code that treats a truthy `field_errors` as "this only happens for X" should be rechecked.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
- **RCS agent registration is self-serve from the SDK.** `client.rcs` gains `registration.get`, `dossier.get`, `brands.create` / `brands.update`, and `agents.create` / `get` / `update` / `set_test_devices` / `submit` / `request_launch`, mirroring the dashboard: draft the brand and agent, submit them for Sendly's review, invite test devices once the agent is in testing, then request launch. Reads need the `rcs:read` scope and writes `rcs:write`; test and live keys both work. Nested hashes (address, contact, basics, campaign, testing) accept snake_case or camelCase keys. Logo, hero and call-to-action media must be public `https://` URLs; assets cannot be uploaded over the API. New models: `Sendly::RcsRegistration` (with `CUSTOMER_STAGES`, `REVIEW_STATUSES` and `ERROR_CODES`), `Sendly::RcsDossier`, `Sendly::RcsBrand`, `Sendly::RcsAddress`, `Sendly::RcsContact`, `Sendly::RcsAgentRegistration`, `Sendly::RcsAgentBasics`, `Sendly::RcsAgentCampaign`, `Sendly::RcsCampaignInteraction`, `Sendly::RcsConsentSettings`, `Sendly::RcsOptInMethod`, `Sendly::RcsAgentTesting` and `Sendly::RcsTestDevice`. `Sendly::RcsAgent` (from `agents.list`) gains `stage`. Every route stays behind the RCS rollout: while it is off for your account these calls raise `Sendly::NotFoundError` with `rcs_not_enabled`.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
dossier = client.rcs.dossier.get
|
|
14
|
+
brand = client.rcs.brands.create(**dossier.brand)
|
|
15
|
+
agent = client.rcs.agents.create(brand_id: brand.id, display_name: "Acme Coffee",
|
|
16
|
+
use_case: "MULTI_USE",
|
|
17
|
+
basics: { logo_url: "https://acme.example/rcs/logo.png" })
|
|
18
|
+
client.rcs.agents.submit(agent.id, idempotency_key: "rcs-submit-#{agent.id}")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **`client.patch` and `client.put` accept `idempotency_key:`.** Neither generates a key on its own (unchanged), but a key you pass is now sent, so the RCS `update` and `set_test_devices` calls can be replayed safely.
|
|
22
|
+
|
|
23
|
+
- **`Sendly::ValidationError#field_errors` carries the API's `errors` list** (`[{ "path", "message" }, ...]`) when a 400 or 422 response includes one, instead of always being `nil`. RCS registration uses it to say which brand, agent, campaign or device field needs attention.
|
|
24
|
+
|
|
3
25
|
## 3.38.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -280,6 +280,32 @@ puts result.explanation # what changed and why
|
|
|
280
280
|
puts result.model # model used (when available)
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
+
## Idempotency
|
|
284
|
+
|
|
285
|
+
Every POST carries an automatically generated `Idempotency-Key` header, held
|
|
286
|
+
across the client's own rate-limit retries, so a retry of a request that
|
|
287
|
+
already reached the API returns the original result instead of sending and
|
|
288
|
+
charging again. Pass your own key (1-255 printable ASCII characters) when the
|
|
289
|
+
guarantee needs to outlive the process, such as a job queue that re-runs after
|
|
290
|
+
a crash or your own retry loop; `idempotency_key:` is accepted on
|
|
291
|
+
`messages.send`, `send_group`, `schedule`, and `send_batch`.
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
message = client.messages.send(
|
|
295
|
+
to: "+15551234567",
|
|
296
|
+
text: "Your order has shipped!",
|
|
297
|
+
idempotency_key: "order-4821-shipped"
|
|
298
|
+
)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Repeating a request with the same key within 24 hours returns the original
|
|
302
|
+
response; `send_batch` sends no automatic key, because the API already
|
|
303
|
+
deduplicates identical batches by their contents. Note this client raises
|
|
304
|
+
`Sendly::TimeoutError` instead of retrying a timeout, so a timeout is exactly
|
|
305
|
+
when to retry with your own key.
|
|
306
|
+
|
|
307
|
+
Full details: https://sendly.live/docs/idempotency
|
|
308
|
+
|
|
283
309
|
## Webhooks
|
|
284
310
|
|
|
285
311
|
```ruby
|
|
@@ -846,9 +872,108 @@ recipients.
|
|
|
846
872
|
|
|
847
873
|
The RCS channel is being rolled out gradually and is not yet generally
|
|
848
874
|
available; until it is enabled for your account the endpoints read as
|
|
849
|
-
absent and calls raise `Sendly::NotFoundError` (HTTP 404
|
|
850
|
-
|
|
851
|
-
|
|
875
|
+
absent and calls raise `Sendly::NotFoundError` (HTTP 404: `not_found` on
|
|
876
|
+
sends and listing, `rcs_not_enabled` on registration). RCS sends and
|
|
877
|
+
capability checks require a live API key.
|
|
878
|
+
|
|
879
|
+
### Registering an agent
|
|
880
|
+
|
|
881
|
+
Registration is self-serve, from the dashboard or the API. Draft the
|
|
882
|
+
brand (the business) and the agent (what recipients see), submit them for
|
|
883
|
+
Sendly's review, and once approved they go on to the carrier network for
|
|
884
|
+
brand verification and agent review. The agent then reaches invited test
|
|
885
|
+
devices only; when you have tested it, request launch and Sendly reviews
|
|
886
|
+
the campaign before sending the launch on to the carrier network. Watch
|
|
887
|
+
`customer_stage` (one of `Sendly::RcsRegistration::CUSTOMER_STAGES`) to
|
|
888
|
+
see where a registration is.
|
|
889
|
+
|
|
890
|
+
Registration reads need the `rcs:read` scope and writes `rcs:write`; test
|
|
891
|
+
and live keys both work, since drafting is not carrier-backed. Logo, hero
|
|
892
|
+
and call-to-action media must be public `https://` URLs: assets cannot be
|
|
893
|
+
uploaded over the API, only from the dashboard. Nested hashes (address,
|
|
894
|
+
contact, basics, campaign, testing) accept snake_case or camelCase keys.
|
|
895
|
+
Every write accepts `idempotency_key:`; `POST`s get an auto-generated key
|
|
896
|
+
as usual, while `PATCH` and `PUT` send one only when you pass it.
|
|
897
|
+
|
|
898
|
+
```ruby
|
|
899
|
+
# Where is the registration at?
|
|
900
|
+
reg = client.rcs.registration.get
|
|
901
|
+
puts reg.stage # "draft" until something is submitted
|
|
902
|
+
|
|
903
|
+
# Seed the brand from details Sendly already holds (your 10DLC brand or
|
|
904
|
+
# toll-free verification), then fill in the rest
|
|
905
|
+
dossier = client.rcs.dossier.get
|
|
906
|
+
brand = client.rcs.brands.create(**dossier.brand) # dossier.source: "tendlc", "verification" or "none"
|
|
907
|
+
client.rcs.brands.update(brand.id,
|
|
908
|
+
display_name: "Acme Coffee",
|
|
909
|
+
legal_entity_type: "LIMITED_LIABILITY_COMPANY",
|
|
910
|
+
contact: { first_name: "Sam", last_name: "Lee", email: "sam@acme.example",
|
|
911
|
+
phone_number: "+15551234567" }
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
# Draft the agent. Media must be public https URLs.
|
|
915
|
+
agent = client.rcs.agents.create(
|
|
916
|
+
brand_id: brand.id,
|
|
917
|
+
display_name: "Acme Coffee",
|
|
918
|
+
use_case: "MULTI_USE",
|
|
919
|
+
basics: {
|
|
920
|
+
description: "Order updates and offers from Acme Coffee",
|
|
921
|
+
logo_url: "https://acme.example/rcs/logo.png",
|
|
922
|
+
hero_url: "https://acme.example/rcs/hero.png",
|
|
923
|
+
brand_color: "#5B3A29",
|
|
924
|
+
privacy_policy_url: "https://acme.example/privacy",
|
|
925
|
+
terms_and_conditions_url: "https://acme.example/terms",
|
|
926
|
+
phone_number: { number: "+15551234567", label: "Support" }
|
|
927
|
+
}
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
# Submit for Sendly's review. Pass your own key if you may retry: a replay
|
|
931
|
+
# returns the original response without submitting again.
|
|
932
|
+
agent = client.rcs.agents.submit(agent.id, idempotency_key: "rcs-submit-#{agent.id}")
|
|
933
|
+
puts agent.review_status # "awaiting_review"
|
|
934
|
+
|
|
935
|
+
# Later: poll, and act on a review note
|
|
936
|
+
agent = client.rcs.agents.get(agent.id)
|
|
937
|
+
if agent.changes_requested?
|
|
938
|
+
puts agent.review_note
|
|
939
|
+
client.rcs.agents.update(agent.id, basics: { hero_url: "https://acme.example/rcs/hero-v2.png" })
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# Once the agent is in testing, invite devices, describe the campaign,
|
|
943
|
+
# then request launch
|
|
944
|
+
client.rcs.agents.set_test_devices(agent.id, devices: [
|
|
945
|
+
{ phone_number: "+15557654321", label: "Sam's Pixel" }
|
|
946
|
+
])
|
|
947
|
+
client.rcs.agents.update(agent.id,
|
|
948
|
+
campaign: {
|
|
949
|
+
company_overview: "Specialty coffee roaster with three cafes in Austin",
|
|
950
|
+
agent_overview: "Order updates, pickup alerts and support replies",
|
|
951
|
+
interactions: [{ interaction_type: "TRANSACTIONAL_UPDATES",
|
|
952
|
+
description: "Order and pickup status" }],
|
|
953
|
+
message_examples: ["Your order #4821 has shipped!",
|
|
954
|
+
"Your latte is ready for pickup at 5th St",
|
|
955
|
+
"Reply HELP for support or STOP to opt out"],
|
|
956
|
+
consent_settings: {
|
|
957
|
+
opt_in_methods: [{ method_type: "WEBSITE", description: "Checkbox at checkout" }],
|
|
958
|
+
call_to_action: "Get order updates by RCS",
|
|
959
|
+
call_to_action_url: "https://acme.example/updates",
|
|
960
|
+
double_opt_in: false,
|
|
961
|
+
help_response: "Acme Coffee: reply STOP to opt out, or email help@acme.example",
|
|
962
|
+
opt_out_response: "You're opted out of Acme Coffee updates."
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
)
|
|
966
|
+
client.rcs.agents.request_launch(agent.id, test_url: "https://acme.example/rcs-test")
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
Registration errors map onto the usual classes: `Sendly::NotFoundError`
|
|
970
|
+
for `rcs_not_enabled` and `rcs_not_found`; `Sendly::ValidationError` for
|
|
971
|
+
`rcs_us_only` and `rcs_invalid_content`, whose `field_errors` is the API's
|
|
972
|
+
list of `{ "path", "message" }` hashes; and `Sendly::APIError` with
|
|
973
|
+
`status_code` 409 for `rcs_field_locked`, `rcs_brand_not_verified` and
|
|
974
|
+
`rcs_launch_not_ready`, or 403 for a key missing the scope.
|
|
975
|
+
|
|
976
|
+
### Sending
|
|
852
977
|
|
|
853
978
|
```ruby
|
|
854
979
|
# Discover your RCS agents ("testing" reaches invited test devices only;
|
data/lib/sendly/client.rb
CHANGED
|
@@ -222,20 +222,28 @@ module Sendly
|
|
|
222
222
|
|
|
223
223
|
# Make a PATCH request
|
|
224
224
|
#
|
|
225
|
+
# No Idempotency-Key is generated for a PATCH; pass +idempotency_key+
|
|
226
|
+
# to send one (1-255 printable ASCII characters).
|
|
227
|
+
#
|
|
225
228
|
# @param path [String] API path
|
|
226
229
|
# @param body [Hash] Request body
|
|
230
|
+
# @param idempotency_key [String, nil] Caller-supplied idempotency key (optional)
|
|
227
231
|
# @return [Hash] Response body
|
|
228
|
-
def patch(path, body = {})
|
|
229
|
-
request(:patch, path, body: body)
|
|
232
|
+
def patch(path, body = {}, idempotency_key: nil)
|
|
233
|
+
request(:patch, path, body: body, idempotency_key: idempotency_key)
|
|
230
234
|
end
|
|
231
235
|
|
|
232
236
|
# Make a PUT request
|
|
233
237
|
#
|
|
238
|
+
# No Idempotency-Key is generated for a PUT; pass +idempotency_key+
|
|
239
|
+
# to send one (1-255 printable ASCII characters).
|
|
240
|
+
#
|
|
234
241
|
# @param path [String] API path
|
|
235
242
|
# @param body [Hash] Request body
|
|
243
|
+
# @param idempotency_key [String, nil] Caller-supplied idempotency key (optional)
|
|
236
244
|
# @return [Hash] Response body
|
|
237
|
-
def put(path, body = {})
|
|
238
|
-
request(:put, path, body: body)
|
|
245
|
+
def put(path, body = {}, idempotency_key: nil)
|
|
246
|
+
request(:put, path, body: body, idempotency_key: idempotency_key)
|
|
239
247
|
end
|
|
240
248
|
|
|
241
249
|
# Make a DELETE request
|
data/lib/sendly/errors.rb
CHANGED
|
@@ -47,7 +47,9 @@ module Sendly
|
|
|
47
47
|
|
|
48
48
|
# Raised when the request contains invalid parameters
|
|
49
49
|
class ValidationError < Error
|
|
50
|
-
# @return [Hash, nil] Field-specific validation errors
|
|
50
|
+
# @return [Array<Hash>, Hash, nil] Field-specific validation errors. For
|
|
51
|
+
# API responses this is the +errors+ list from the body when present,
|
|
52
|
+
# e.g. +[{ "path" => "brand.ein", "message" => "Enter a 9-digit EIN" }]+
|
|
51
53
|
attr_reader :field_errors
|
|
52
54
|
|
|
53
55
|
def initialize(message = "Validation failed", field_errors: nil, details: nil)
|
|
@@ -101,7 +103,7 @@ module Sendly
|
|
|
101
103
|
|
|
102
104
|
case status
|
|
103
105
|
when 400, 422
|
|
104
|
-
ValidationError.new(message, details: details)
|
|
106
|
+
ValidationError.new(message, details: details, field_errors: body["errors"])
|
|
105
107
|
when 401
|
|
106
108
|
AuthenticationError.new(message)
|
|
107
109
|
when 402
|
data/lib/sendly/rcs_resource.rb
CHANGED
|
@@ -6,8 +6,10 @@ module Sendly
|
|
|
6
6
|
# +"testing"+ (can send, but only to invited test devices) → +"approved"+
|
|
7
7
|
# (can send to everyone); +"suspended"+ agents cannot send. +sendable+ is
|
|
8
8
|
# true when the agent is fully provisioned and its status allows sending.
|
|
9
|
+
# +stage+ is the registration stage, one of
|
|
10
|
+
# {RcsRegistration::CUSTOMER_STAGES}.
|
|
9
11
|
class RcsAgent
|
|
10
|
-
attr_reader :id, :name, :status, :use_case, :sendable, :created_at
|
|
12
|
+
attr_reader :id, :name, :status, :use_case, :sendable, :stage, :created_at
|
|
11
13
|
|
|
12
14
|
STATUSES = %w[draft submitted testing approved suspended].freeze
|
|
13
15
|
|
|
@@ -17,6 +19,7 @@ module Sendly
|
|
|
17
19
|
@status = data["status"]
|
|
18
20
|
@use_case = data["useCase"] || data["use_case"]
|
|
19
21
|
@sendable = data["sendable"] || false
|
|
22
|
+
@stage = data["stage"]
|
|
20
23
|
@created_at = data["createdAt"] || data["created_at"]
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -31,7 +34,7 @@ module Sendly
|
|
|
31
34
|
def to_h
|
|
32
35
|
{
|
|
33
36
|
id: id, name: name, status: status, use_case: use_case,
|
|
34
|
-
sendable: sendable, created_at: created_at
|
|
37
|
+
sendable: sendable, stage: stage, created_at: created_at
|
|
35
38
|
}.compact
|
|
36
39
|
end
|
|
37
40
|
end
|
|
@@ -135,14 +138,703 @@ module Sendly
|
|
|
135
138
|
end
|
|
136
139
|
end
|
|
137
140
|
|
|
138
|
-
#
|
|
141
|
+
# Shapes RCS registration input before it leaves the process. Nested
|
|
142
|
+
# hashes (address, contact, basics, campaign, testing, devices) accept
|
|
143
|
+
# either snake_case or camelCase keys; keys are camelised and values are
|
|
144
|
+
# passed through untouched.
|
|
145
|
+
#
|
|
146
|
+
# @api private
|
|
147
|
+
module RcsRegistrationInput
|
|
148
|
+
OMIT = Object.new.freeze
|
|
149
|
+
|
|
150
|
+
module_function
|
|
151
|
+
|
|
152
|
+
def omitted?(value)
|
|
153
|
+
OMIT.equal?(value)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def camelize(value)
|
|
157
|
+
case value
|
|
158
|
+
when Hash
|
|
159
|
+
value.each_with_object({}) { |(k, v), out| out[camel_key(k)] = camelize(v) }
|
|
160
|
+
when Array
|
|
161
|
+
value.map { |v| camelize(v) }
|
|
162
|
+
else
|
|
163
|
+
value
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def underscore(value)
|
|
168
|
+
case value
|
|
169
|
+
when Hash
|
|
170
|
+
value.each_with_object({}) { |(k, v), out| out[snake_key(k)] = underscore(v) }
|
|
171
|
+
when Array
|
|
172
|
+
value.map { |v| underscore(v) }
|
|
173
|
+
else
|
|
174
|
+
value
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def camel_key(key)
|
|
179
|
+
key.to_s.gsub(/_([a-z0-9])/) { Regexp.last_match(1).upcase }.to_sym
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def snake_key(key)
|
|
183
|
+
key.to_s.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }.to_sym
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def encode_id!(id, label)
|
|
187
|
+
raise ValidationError, "#{label} is required" if id.nil? || id.to_s.empty?
|
|
188
|
+
|
|
189
|
+
URI.encode_www_form_component(id)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# A postal address on an RCS brand. +country_code+ is always "US": RCS
|
|
194
|
+
# registration is open to US businesses for now.
|
|
195
|
+
class RcsAddress
|
|
196
|
+
attr_reader :line1, :line2, :city, :state, :postal_code, :country_code
|
|
197
|
+
|
|
198
|
+
def initialize(data)
|
|
199
|
+
data ||= {}
|
|
200
|
+
@line1 = data["line1"]
|
|
201
|
+
@line2 = data["line2"]
|
|
202
|
+
@city = data["city"]
|
|
203
|
+
@state = data["state"]
|
|
204
|
+
@postal_code = data["postalCode"] || data["postal_code"]
|
|
205
|
+
@country_code = data["countryCode"] || data["country_code"]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def to_h
|
|
209
|
+
{
|
|
210
|
+
line1: line1, line2: line2, city: city, state: state,
|
|
211
|
+
postal_code: postal_code, country_code: country_code
|
|
212
|
+
}.compact
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# The business contact on an RCS brand.
|
|
217
|
+
class RcsContact
|
|
218
|
+
attr_reader :first_name, :last_name, :title, :email, :phone_number
|
|
219
|
+
|
|
220
|
+
def initialize(data)
|
|
221
|
+
data ||= {}
|
|
222
|
+
@first_name = data["firstName"] || data["first_name"]
|
|
223
|
+
@last_name = data["lastName"] || data["last_name"]
|
|
224
|
+
@title = data["title"]
|
|
225
|
+
@email = data["email"]
|
|
226
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def to_h
|
|
230
|
+
{
|
|
231
|
+
first_name: first_name, last_name: last_name, title: title,
|
|
232
|
+
email: email, phone_number: phone_number
|
|
233
|
+
}.compact
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The business behind an RCS agent. +review_status+ starts +"draft"+,
|
|
238
|
+
# becomes +"awaiting_review"+ once an agent under it is submitted, and
|
|
239
|
+
# moves to +"approved_for_carrier"+ (Sendly approved it and sent it on to
|
|
240
|
+
# the carrier network), +"changes_requested"+ (see +review_note+) or
|
|
241
|
+
# +"rejected"+ (see +rejection_reason+). +verified_at+ is set once the
|
|
242
|
+
# carrier network has verified the business. +customer_stage+ is the
|
|
243
|
+
# same lifecycle summarised as one of {RcsRegistration::CUSTOMER_STAGES}.
|
|
244
|
+
class RcsBrand
|
|
245
|
+
LEGAL_ENTITY_TYPES = %w[LIMITED_LIABILITY_COMPANY SOLE_PROPRIETORSHIP PARTNERSHIP
|
|
246
|
+
CORPORATION S_CORPORATION].freeze
|
|
247
|
+
ORGANIZATION_TYPES = %w[PRIVATE_PROFIT PUBLIC_PROFIT NON_PROFIT GOVERNMENT UNKNOWN].freeze
|
|
248
|
+
|
|
249
|
+
attr_reader :id, :review_status, :customer_stage, :display_name,
|
|
250
|
+
:legal_name, :legal_entity_type, :organization_type,
|
|
251
|
+
:stock_symbol, :website_url, :ein, :address, :contact,
|
|
252
|
+
:review_note, :rejection_reason, :submitted_for_review_at,
|
|
253
|
+
:sent_to_carrier_at, :verified_at, :created_at, :updated_at
|
|
254
|
+
|
|
255
|
+
def initialize(data)
|
|
256
|
+
@id = data["id"]
|
|
257
|
+
@review_status = data["reviewStatus"] || data["review_status"]
|
|
258
|
+
@customer_stage = data["customerStage"] || data["customer_stage"]
|
|
259
|
+
@display_name = data["displayName"] || data["display_name"]
|
|
260
|
+
@legal_name = data["legalName"] || data["legal_name"]
|
|
261
|
+
@legal_entity_type = data["legalEntityType"] || data["legal_entity_type"]
|
|
262
|
+
@organization_type = data["organizationType"] || data["organization_type"]
|
|
263
|
+
@stock_symbol = data["stockSymbol"] || data["stock_symbol"]
|
|
264
|
+
@website_url = data["websiteUrl"] || data["website_url"]
|
|
265
|
+
@ein = data["ein"]
|
|
266
|
+
@address = data["address"] ? RcsAddress.new(data["address"]) : nil
|
|
267
|
+
@contact = data["contact"] ? RcsContact.new(data["contact"]) : nil
|
|
268
|
+
@review_note = data["reviewNote"] || data["review_note"]
|
|
269
|
+
@rejection_reason = data["rejectionReason"] || data["rejection_reason"]
|
|
270
|
+
@submitted_for_review_at = data["submittedForReviewAt"] || data["submitted_for_review_at"]
|
|
271
|
+
@sent_to_carrier_at = data["sentToCarrierAt"] || data["sent_to_carrier_at"]
|
|
272
|
+
@verified_at = data["verifiedAt"] || data["verified_at"]
|
|
273
|
+
@created_at = data["createdAt"] || data["created_at"]
|
|
274
|
+
@updated_at = data["updatedAt"] || data["updated_at"]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def draft?
|
|
278
|
+
review_status == "draft"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def awaiting_review?
|
|
282
|
+
review_status == "awaiting_review"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def changes_requested?
|
|
286
|
+
review_status == "changes_requested"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def rejected?
|
|
290
|
+
review_status == "rejected"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def verified?
|
|
294
|
+
!verified_at.nil?
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# True while the brand can still be edited (draft or changes requested).
|
|
298
|
+
def editable?
|
|
299
|
+
draft? || changes_requested?
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def to_h
|
|
303
|
+
{
|
|
304
|
+
id: id, review_status: review_status, customer_stage: customer_stage,
|
|
305
|
+
display_name: display_name, legal_name: legal_name,
|
|
306
|
+
legal_entity_type: legal_entity_type, organization_type: organization_type,
|
|
307
|
+
stock_symbol: stock_symbol, website_url: website_url, ein: ein,
|
|
308
|
+
address: address&.to_h, contact: contact&.to_h,
|
|
309
|
+
review_note: review_note, rejection_reason: rejection_reason,
|
|
310
|
+
submitted_for_review_at: submitted_for_review_at,
|
|
311
|
+
sent_to_carrier_at: sent_to_carrier_at, verified_at: verified_at,
|
|
312
|
+
created_at: created_at, updated_at: updated_at
|
|
313
|
+
}.compact
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A phone that can receive an agent's messages while it is in testing.
|
|
318
|
+
# +invite_status+ is nil until the carrier network has been asked to
|
|
319
|
+
# invite the device.
|
|
320
|
+
class RcsTestDevice
|
|
321
|
+
attr_reader :id, :phone_number, :label, :invite_status, :created_at
|
|
322
|
+
|
|
323
|
+
def initialize(data)
|
|
324
|
+
@id = data["id"]
|
|
325
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
326
|
+
@label = data["label"]
|
|
327
|
+
@invite_status = data["inviteStatus"] || data["invite_status"]
|
|
328
|
+
@created_at = data["createdAt"] || data["created_at"]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def invited?
|
|
332
|
+
!invite_status.nil?
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def to_h
|
|
336
|
+
{
|
|
337
|
+
id: id, phone_number: phone_number, label: label,
|
|
338
|
+
invite_status: invite_status, created_at: created_at
|
|
339
|
+
}.compact
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# What recipients see of an agent: name, use case, description, logo and
|
|
344
|
+
# hero images, colour, policy links and contact entries. +phone_number+,
|
|
345
|
+
# +website+ and +email+ are hashes as stored (+"number"+ / +"url"+ /
|
|
346
|
+
# +"address"+ plus +"label"+). +logo_url+ and +hero_url+ must be public
|
|
347
|
+
# https URLs; assets cannot be uploaded over the API.
|
|
348
|
+
class RcsAgentBasics
|
|
349
|
+
attr_reader :display_name, :use_case, :hosting_region, :description,
|
|
350
|
+
:logo_url, :hero_url, :brand_color, :privacy_policy_url,
|
|
351
|
+
:terms_and_conditions_url, :phone_number, :website, :email
|
|
352
|
+
|
|
353
|
+
def initialize(data)
|
|
354
|
+
data ||= {}
|
|
355
|
+
@display_name = data["displayName"] || data["display_name"]
|
|
356
|
+
@use_case = data["useCase"] || data["use_case"]
|
|
357
|
+
@hosting_region = data["hostingRegion"] || data["hosting_region"]
|
|
358
|
+
@description = data["description"]
|
|
359
|
+
@logo_url = data["logoUrl"] || data["logo_url"]
|
|
360
|
+
@hero_url = data["heroUrl"] || data["hero_url"]
|
|
361
|
+
@brand_color = data["brandColor"] || data["brand_color"]
|
|
362
|
+
@privacy_policy_url = data["privacyPolicyUrl"] || data["privacy_policy_url"]
|
|
363
|
+
@terms_and_conditions_url = data["termsAndConditionsUrl"] || data["terms_and_conditions_url"]
|
|
364
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
365
|
+
@website = data["website"]
|
|
366
|
+
@email = data["email"]
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def to_h
|
|
370
|
+
{
|
|
371
|
+
display_name: display_name, use_case: use_case,
|
|
372
|
+
hosting_region: hosting_region, description: description,
|
|
373
|
+
logo_url: logo_url, hero_url: hero_url, brand_color: brand_color,
|
|
374
|
+
privacy_policy_url: privacy_policy_url,
|
|
375
|
+
terms_and_conditions_url: terms_and_conditions_url,
|
|
376
|
+
phone_number: phone_number, website: website, email: email
|
|
377
|
+
}.compact
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# One kind of conversation the agent will have with recipients.
|
|
382
|
+
class RcsCampaignInteraction
|
|
383
|
+
INTERACTION_TYPES = %w[TRANSACTIONAL_UPDATES CUSTOMER_SUPPORT LOYALTY_OR_REWARD
|
|
384
|
+
MARKETING_OR_PROMOTIONAL ACCOUNT_ALERTS TWO_WAY_CONVERSATION
|
|
385
|
+
OTHER].freeze
|
|
386
|
+
|
|
387
|
+
attr_reader :interaction_type, :description
|
|
388
|
+
|
|
389
|
+
def initialize(data)
|
|
390
|
+
data ||= {}
|
|
391
|
+
@interaction_type = data["interactionType"] || data["interaction_type"]
|
|
392
|
+
@description = data["description"]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def to_h
|
|
396
|
+
{ interaction_type: interaction_type, description: description }.compact
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# One way recipients opt in to the agent's messages.
|
|
401
|
+
class RcsOptInMethod
|
|
402
|
+
METHOD_TYPES = %w[SMS WEBSITE MOBILE_APP QR_CODE SALE_POINT OTHER].freeze
|
|
403
|
+
|
|
404
|
+
attr_reader :method_type, :description
|
|
405
|
+
|
|
406
|
+
def initialize(data)
|
|
407
|
+
data ||= {}
|
|
408
|
+
@method_type = data["methodType"] || data["method_type"]
|
|
409
|
+
@description = data["description"]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def to_h
|
|
413
|
+
{ method_type: method_type, description: description }.compact
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# How recipients consent to, get help with, and stop the agent's
|
|
418
|
+
# messages. +call_to_action_media_url+ must be a public https URL.
|
|
419
|
+
class RcsConsentSettings
|
|
420
|
+
attr_reader :opt_in_methods, :call_to_action, :call_to_action_url,
|
|
421
|
+
:call_to_action_media_url, :double_opt_in,
|
|
422
|
+
:double_opt_in_message, :opt_in_message, :help_response,
|
|
423
|
+
:opt_out_response
|
|
424
|
+
|
|
425
|
+
def initialize(data)
|
|
426
|
+
data ||= {}
|
|
427
|
+
methods = data["optInMethods"] || data["opt_in_methods"]
|
|
428
|
+
@opt_in_methods = methods ? methods.map { |m| RcsOptInMethod.new(m) } : nil
|
|
429
|
+
@call_to_action = data["callToAction"] || data["call_to_action"]
|
|
430
|
+
@call_to_action_url = data["callToActionUrl"] || data["call_to_action_url"]
|
|
431
|
+
@call_to_action_media_url = data["callToActionMediaUrl"] || data["call_to_action_media_url"]
|
|
432
|
+
@double_opt_in = data.key?("doubleOptIn") ? data["doubleOptIn"] : data["double_opt_in"]
|
|
433
|
+
@double_opt_in_message = data["doubleOptInMessage"] || data["double_opt_in_message"]
|
|
434
|
+
@opt_in_message = data["optInMessage"] || data["opt_in_message"]
|
|
435
|
+
@help_response = data["helpResponse"] || data["help_response"]
|
|
436
|
+
@opt_out_response = data["optOutResponse"] || data["opt_out_response"]
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def to_h
|
|
440
|
+
{
|
|
441
|
+
opt_in_methods: opt_in_methods&.map(&:to_h), call_to_action: call_to_action,
|
|
442
|
+
call_to_action_url: call_to_action_url,
|
|
443
|
+
call_to_action_media_url: call_to_action_media_url,
|
|
444
|
+
double_opt_in: double_opt_in, double_opt_in_message: double_opt_in_message,
|
|
445
|
+
opt_in_message: opt_in_message, help_response: help_response,
|
|
446
|
+
opt_out_response: opt_out_response
|
|
447
|
+
}.compact
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# The agent's campaign: what the business does, what the agent sends,
|
|
452
|
+
# example messages and consent settings. Reviewed before launch.
|
|
453
|
+
class RcsAgentCampaign
|
|
454
|
+
attr_reader :company_overview, :agent_overview, :additional_information,
|
|
455
|
+
:interactions, :message_examples, :consent_settings
|
|
456
|
+
|
|
457
|
+
def initialize(data)
|
|
458
|
+
data ||= {}
|
|
459
|
+
@company_overview = data["companyOverview"] || data["company_overview"]
|
|
460
|
+
@agent_overview = data["agentOverview"] || data["agent_overview"]
|
|
461
|
+
@additional_information = data["additionalInformation"] || data["additional_information"]
|
|
462
|
+
interactions = data["interactions"]
|
|
463
|
+
@interactions = interactions ? interactions.map { |i| RcsCampaignInteraction.new(i) } : nil
|
|
464
|
+
@message_examples = data["messageExamples"] || data["message_examples"]
|
|
465
|
+
consent = data["consentSettings"] || data["consent_settings"]
|
|
466
|
+
@consent_settings = consent ? RcsConsentSettings.new(consent) : nil
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def to_h
|
|
470
|
+
{
|
|
471
|
+
company_overview: company_overview, agent_overview: agent_overview,
|
|
472
|
+
additional_information: additional_information,
|
|
473
|
+
interactions: interactions&.map(&:to_h),
|
|
474
|
+
message_examples: message_examples,
|
|
475
|
+
consent_settings: consent_settings&.to_h
|
|
476
|
+
}.compact
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# How the agent was tested on an invited device before launch.
|
|
481
|
+
class RcsAgentTesting
|
|
482
|
+
attr_reader :test_url, :message_id, :additional_information
|
|
483
|
+
|
|
484
|
+
def initialize(data)
|
|
485
|
+
data ||= {}
|
|
486
|
+
@test_url = data["testUrl"] || data["test_url"]
|
|
487
|
+
@message_id = data["messageId"] || data["message_id"]
|
|
488
|
+
@additional_information = data["additionalInformation"] || data["additional_information"]
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def to_h
|
|
492
|
+
{
|
|
493
|
+
test_url: test_url, message_id: message_id,
|
|
494
|
+
additional_information: additional_information
|
|
495
|
+
}.compact
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# The full registration record of an RCS agent: its basics, campaign,
|
|
500
|
+
# testing details, test devices and review state. +status+ is the send
|
|
501
|
+
# status also shown by {RcsAgentsResource#list} ("draft", "submitted",
|
|
502
|
+
# "testing", "approved", "suspended"); +review_status+ tracks the
|
|
503
|
+
# registration itself, one of {RcsRegistration::REVIEW_STATUSES}, and
|
|
504
|
+
# +customer_stage+ summarises both with the brand as one of
|
|
505
|
+
# {RcsRegistration::CUSTOMER_STAGES}. +review_note+ carries Sendly's
|
|
506
|
+
# note when changes are requested and +rejection_reason+ the carrier
|
|
507
|
+
# network's reason when it rejects the agent.
|
|
508
|
+
class RcsAgentRegistration
|
|
509
|
+
USE_CASES = %w[MULTI_USE PROMOTIONAL TRANSACTIONAL OTP].freeze
|
|
510
|
+
|
|
511
|
+
attr_reader :id, :brand_id, :status, :review_status, :customer_stage,
|
|
512
|
+
:display_name, :use_case, :hosting_region, :basics,
|
|
513
|
+
:campaign, :testing, :review_note, :rejection_reason,
|
|
514
|
+
:test_devices, :submitted_for_review_at,
|
|
515
|
+
:basics_submitted_at, :launch_submitted_at, :live_at,
|
|
516
|
+
:created_at, :updated_at
|
|
517
|
+
|
|
518
|
+
def initialize(data)
|
|
519
|
+
@id = data["id"]
|
|
520
|
+
@brand_id = data["brandId"] || data["brand_id"]
|
|
521
|
+
@status = data["status"]
|
|
522
|
+
@review_status = data["reviewStatus"] || data["review_status"]
|
|
523
|
+
@customer_stage = data["customerStage"] || data["customer_stage"]
|
|
524
|
+
@display_name = data["displayName"] || data["display_name"]
|
|
525
|
+
@use_case = data["useCase"] || data["use_case"]
|
|
526
|
+
@hosting_region = data["hostingRegion"] || data["hosting_region"]
|
|
527
|
+
@basics = data["basics"] ? RcsAgentBasics.new(data["basics"]) : nil
|
|
528
|
+
@campaign = data["campaign"] ? RcsAgentCampaign.new(data["campaign"]) : nil
|
|
529
|
+
@testing = data["testing"] ? RcsAgentTesting.new(data["testing"]) : nil
|
|
530
|
+
@review_note = data["reviewNote"] || data["review_note"]
|
|
531
|
+
@rejection_reason = data["rejectionReason"] || data["rejection_reason"]
|
|
532
|
+
devices = data["testDevices"] || data["test_devices"] || []
|
|
533
|
+
@test_devices = devices.map { |d| RcsTestDevice.new(d) }
|
|
534
|
+
@submitted_for_review_at = data["submittedForReviewAt"] || data["submitted_for_review_at"]
|
|
535
|
+
@basics_submitted_at = data["basicsSubmittedAt"] || data["basics_submitted_at"]
|
|
536
|
+
@launch_submitted_at = data["launchSubmittedAt"] || data["launch_submitted_at"]
|
|
537
|
+
@live_at = data["liveAt"] || data["live_at"]
|
|
538
|
+
@created_at = data["createdAt"] || data["created_at"]
|
|
539
|
+
@updated_at = data["updatedAt"] || data["updated_at"]
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def draft?
|
|
543
|
+
review_status == "draft"
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def awaiting_review?
|
|
547
|
+
review_status == "awaiting_review"
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def changes_requested?
|
|
551
|
+
review_status == "changes_requested"
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def approved_for_carrier?
|
|
555
|
+
review_status == "approved_for_carrier"
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def launch_requested?
|
|
559
|
+
review_status == "launch_requested"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def launch_rejected?
|
|
563
|
+
review_status == "launch_rejected"
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def rejected?
|
|
567
|
+
review_status == "rejected"
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# True while the agent reaches invited test devices only.
|
|
571
|
+
def testing?
|
|
572
|
+
status == "testing"
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def live?
|
|
576
|
+
customer_stage == "live"
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# True while the agent's basics can still be edited and it can be
|
|
580
|
+
# submitted for review.
|
|
581
|
+
def editable?
|
|
582
|
+
draft? || changes_requested?
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def to_h
|
|
586
|
+
{
|
|
587
|
+
id: id, brand_id: brand_id, status: status,
|
|
588
|
+
review_status: review_status, customer_stage: customer_stage,
|
|
589
|
+
display_name: display_name, use_case: use_case,
|
|
590
|
+
hosting_region: hosting_region, basics: basics&.to_h,
|
|
591
|
+
campaign: campaign&.to_h, testing: testing&.to_h,
|
|
592
|
+
review_note: review_note, rejection_reason: rejection_reason,
|
|
593
|
+
test_devices: test_devices.map(&:to_h),
|
|
594
|
+
submitted_for_review_at: submitted_for_review_at,
|
|
595
|
+
basics_submitted_at: basics_submitted_at,
|
|
596
|
+
launch_submitted_at: launch_submitted_at, live_at: live_at,
|
|
597
|
+
created_at: created_at, updated_at: updated_at
|
|
598
|
+
}.compact
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# The workspace's RCS registration at a glance: the newest agent, its
|
|
603
|
+
# brand and test devices, and the +stage+ they are at together. +brand+
|
|
604
|
+
# and +agent+ are nil until something has been drafted, and +stage+ is
|
|
605
|
+
# then "draft". +us_eligible+ is false when something on file names a
|
|
606
|
+
# non-US country.
|
|
607
|
+
class RcsRegistration
|
|
608
|
+
CUSTOMER_STAGES = %w[draft in_review changes_requested rejected brand_verification
|
|
609
|
+
agent_review testing launch_review launching launch_rejected
|
|
610
|
+
live suspended failed].freeze
|
|
611
|
+
REVIEW_STATUSES = %w[draft awaiting_review changes_requested approved_for_carrier
|
|
612
|
+
rejected launch_requested launch_submitted launch_rejected
|
|
613
|
+
failed].freeze
|
|
614
|
+
ERROR_CODES = %w[rcs_not_enabled rcs_not_found rcs_field_locked rcs_us_only
|
|
615
|
+
rcs_invalid_content rcs_brand_not_verified rcs_launch_not_ready
|
|
616
|
+
rcs_internal_error].freeze
|
|
617
|
+
|
|
618
|
+
attr_reader :brand, :agent, :devices, :stage, :us_eligible
|
|
619
|
+
|
|
620
|
+
def initialize(data)
|
|
621
|
+
@brand = data["brand"] ? RcsBrand.new(data["brand"]) : nil
|
|
622
|
+
@agent = data["agent"] ? RcsAgentRegistration.new(data["agent"]) : nil
|
|
623
|
+
@devices = (data["devices"] || []).map { |d| RcsTestDevice.new(d) }
|
|
624
|
+
@stage = data["stage"]
|
|
625
|
+
@us_eligible = data.key?("usEligible") ? data["usEligible"] : data["us_eligible"]
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def us_eligible?
|
|
629
|
+
us_eligible == true
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def live?
|
|
633
|
+
stage == "live"
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def to_h
|
|
637
|
+
{
|
|
638
|
+
brand: brand&.to_h, agent: agent&.to_h,
|
|
639
|
+
devices: devices.map(&:to_h), stage: stage, us_eligible: us_eligible
|
|
640
|
+
}.compact
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Business details Sendly already holds for the workspace, ready to seed
|
|
645
|
+
# an RCS brand. +brand+ is a snake_case hash of only the fields on file
|
|
646
|
+
# (+legal_name+, +display_name+, +ein+, +organization_type+,
|
|
647
|
+
# +website_url+, +address+, +contact+), so it can be passed straight to
|
|
648
|
+
# {RcsBrandsResource#create} with +**dossier.brand+. +source+ says where
|
|
649
|
+
# the details came from: "tendlc" (the workspace's newest 10DLC brand),
|
|
650
|
+
# "verification" (its active toll-free verification) or "none".
|
|
651
|
+
class RcsDossier
|
|
652
|
+
SOURCES = %w[tendlc verification none].freeze
|
|
653
|
+
|
|
654
|
+
attr_reader :brand, :us_eligible, :source
|
|
655
|
+
|
|
656
|
+
def initialize(data)
|
|
657
|
+
@brand = RcsRegistrationInput.underscore(data["brand"] || {})
|
|
658
|
+
@us_eligible = data.key?("usEligible") ? data["usEligible"] : data["us_eligible"]
|
|
659
|
+
@source = data["source"]
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def us_eligible?
|
|
663
|
+
us_eligible == true
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def prefilled?
|
|
667
|
+
!brand.empty?
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def to_h
|
|
671
|
+
{ brand: brand, us_eligible: us_eligible, source: source }.compact
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Read the workspace's RCS registration as a whole.
|
|
676
|
+
class RcsRegistrationResource
|
|
677
|
+
def initialize(client)
|
|
678
|
+
@client = client
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Fetch the registration: the newest agent, its brand and test devices,
|
|
682
|
+
# and the stage they are at. Requires the +rcs:read+ scope.
|
|
683
|
+
#
|
|
684
|
+
# @return [RcsRegistration]
|
|
685
|
+
#
|
|
686
|
+
# @example
|
|
687
|
+
# reg = client.rcs.registration.get
|
|
688
|
+
# puts reg.stage # "draft" until something is submitted
|
|
689
|
+
# puts reg.agent&.review_status # e.g. "awaiting_review"
|
|
690
|
+
def get
|
|
691
|
+
response = @client.get("/rcs/registration")
|
|
692
|
+
RcsRegistration.new(response)
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Business details already on file, to seed an RCS brand from.
|
|
697
|
+
class RcsDossierResource
|
|
698
|
+
def initialize(client)
|
|
699
|
+
@client = client
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Fetch the details Sendly already holds for this workspace (from its
|
|
703
|
+
# 10DLC brand or toll-free verification), compacted to the fields that
|
|
704
|
+
# are set. Requires the +rcs:read+ scope.
|
|
705
|
+
#
|
|
706
|
+
# @return [RcsDossier]
|
|
707
|
+
#
|
|
708
|
+
# @example Seed a brand from the dossier
|
|
709
|
+
# dossier = client.rcs.dossier.get
|
|
710
|
+
# brand = client.rcs.brands.create(**dossier.brand) if dossier.prefilled?
|
|
711
|
+
def get
|
|
712
|
+
response = @client.get("/rcs/dossier")
|
|
713
|
+
RcsDossier.new(response)
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Draft and edit the business behind your RCS agents.
|
|
718
|
+
class RcsBrandsResource
|
|
719
|
+
def initialize(client)
|
|
720
|
+
@client = client
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# Draft a brand. Every field is optional here; required fields are
|
|
724
|
+
# checked when an agent under the brand is submitted for review. The
|
|
725
|
+
# address must be in the US. Requires the +rcs:write+ scope.
|
|
726
|
+
#
|
|
727
|
+
# Nested hashes accept snake_case or camelCase keys.
|
|
728
|
+
#
|
|
729
|
+
# @param display_name [String, nil] The brand name recipients see
|
|
730
|
+
# @param legal_name [String, nil] Registered legal name
|
|
731
|
+
# @param legal_entity_type [String, nil] One of {RcsBrand::LEGAL_ENTITY_TYPES}
|
|
732
|
+
# @param organization_type [String, nil] One of {RcsBrand::ORGANIZATION_TYPES}
|
|
733
|
+
# @param website_url [String, nil] Public https website
|
|
734
|
+
# @param ein [String, nil] Employer identification number ("12-3456789" or "123456789")
|
|
735
|
+
# @param stock_symbol [String, nil] "EXCHANGE:TICKER" for public companies
|
|
736
|
+
# @param address [Hash, nil] +{ line1:, line2:, city:, state:, postal_code:, country_code: "US" }+
|
|
737
|
+
# @param contact [Hash, nil] +{ first_name:, last_name:, title:, email:, phone_number: }+ (E.164 phone)
|
|
738
|
+
# @param idempotency_key [String, nil] Idempotency key for this operation
|
|
739
|
+
# @return [RcsBrand]
|
|
740
|
+
# @raise [Sendly::ValidationError] HTTP 422 +rcs_us_only+ when the address is outside the US
|
|
741
|
+
#
|
|
742
|
+
# @example
|
|
743
|
+
# brand = client.rcs.brands.create(
|
|
744
|
+
# display_name: "Acme Coffee",
|
|
745
|
+
# legal_name: "Acme Holdings LLC",
|
|
746
|
+
# legal_entity_type: "LIMITED_LIABILITY_COMPANY",
|
|
747
|
+
# organization_type: "PRIVATE_PROFIT",
|
|
748
|
+
# website_url: "https://acme.example",
|
|
749
|
+
# ein: "12-3456789",
|
|
750
|
+
# address: { line1: "1 Main St", city: "Austin", state: "TX",
|
|
751
|
+
# postal_code: "78701", country_code: "US" },
|
|
752
|
+
# contact: { first_name: "Sam", last_name: "Lee", email: "sam@acme.example",
|
|
753
|
+
# phone_number: "+15551234567" }
|
|
754
|
+
# )
|
|
755
|
+
def create(display_name: nil, legal_name: nil, legal_entity_type: nil,
|
|
756
|
+
organization_type: nil, website_url: nil, ein: nil,
|
|
757
|
+
stock_symbol: nil, address: nil, contact: nil,
|
|
758
|
+
idempotency_key: nil)
|
|
759
|
+
body = {}
|
|
760
|
+
body[:displayName] = display_name unless display_name.nil?
|
|
761
|
+
body[:legalName] = legal_name unless legal_name.nil?
|
|
762
|
+
body[:legalEntityType] = legal_entity_type unless legal_entity_type.nil?
|
|
763
|
+
body[:organizationType] = organization_type unless organization_type.nil?
|
|
764
|
+
body[:websiteUrl] = website_url unless website_url.nil?
|
|
765
|
+
body[:ein] = ein unless ein.nil?
|
|
766
|
+
body[:stockSymbol] = stock_symbol unless stock_symbol.nil?
|
|
767
|
+
body[:address] = RcsRegistrationInput.camelize(address) unless address.nil?
|
|
768
|
+
body[:contact] = RcsRegistrationInput.camelize(contact) unless contact.nil?
|
|
769
|
+
|
|
770
|
+
response = @client.post("/rcs/brands", body, idempotency_key: idempotency_key)
|
|
771
|
+
RcsBrand.new(response["brand"] || {})
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Edit a brand that is still editable (draft or changes requested).
|
|
775
|
+
# Only the fields you pass are changed; pass +nil+ to clear a nullable
|
|
776
|
+
# field. +address+ and +contact+ may be partial hashes. Requires the
|
|
777
|
+
# +rcs:write+ scope.
|
|
778
|
+
#
|
|
779
|
+
# @param id [String] Brand identifier
|
|
780
|
+
# @param display_name [String, nil]
|
|
781
|
+
# @param legal_name [String, nil]
|
|
782
|
+
# @param legal_entity_type [String, nil] One of {RcsBrand::LEGAL_ENTITY_TYPES}
|
|
783
|
+
# @param organization_type [String, nil] One of {RcsBrand::ORGANIZATION_TYPES}
|
|
784
|
+
# @param website_url [String, nil]
|
|
785
|
+
# @param ein [String, nil]
|
|
786
|
+
# @param stock_symbol [String, nil]
|
|
787
|
+
# @param address [Hash, nil] Partial address; +country_code+, when present, must be "US"
|
|
788
|
+
# @param contact [Hash, nil] Partial contact
|
|
789
|
+
# @param idempotency_key [String, nil] Idempotency key for this operation
|
|
790
|
+
# @return [RcsBrand] The updated brand
|
|
791
|
+
# @raise [Sendly::ValidationError] If no field is given, or HTTP 422
|
|
792
|
+
# (+rcs_us_only+, +rcs_invalid_content+ with +field_errors+)
|
|
793
|
+
# @raise [Sendly::NotFoundError] HTTP 404 +rcs_not_found+ when the brand isn't in this workspace
|
|
794
|
+
# @raise [Sendly::APIError] HTTP 409 +rcs_field_locked+ while the brand is under review
|
|
795
|
+
#
|
|
796
|
+
# @example
|
|
797
|
+
# client.rcs.brands.update(brand.id, website_url: "https://acme.example",
|
|
798
|
+
# address: { line2: "Suite 4" })
|
|
799
|
+
def update(id, display_name: RcsRegistrationInput::OMIT,
|
|
800
|
+
legal_name: RcsRegistrationInput::OMIT,
|
|
801
|
+
legal_entity_type: RcsRegistrationInput::OMIT,
|
|
802
|
+
organization_type: RcsRegistrationInput::OMIT,
|
|
803
|
+
website_url: RcsRegistrationInput::OMIT,
|
|
804
|
+
ein: RcsRegistrationInput::OMIT,
|
|
805
|
+
stock_symbol: RcsRegistrationInput::OMIT,
|
|
806
|
+
address: RcsRegistrationInput::OMIT,
|
|
807
|
+
contact: RcsRegistrationInput::OMIT,
|
|
808
|
+
idempotency_key: nil)
|
|
809
|
+
encoded_id = RcsRegistrationInput.encode_id!(id, "Brand ID")
|
|
810
|
+
|
|
811
|
+
body = {}
|
|
812
|
+
body[:displayName] = display_name unless RcsRegistrationInput.omitted?(display_name)
|
|
813
|
+
body[:legalName] = legal_name unless RcsRegistrationInput.omitted?(legal_name)
|
|
814
|
+
body[:legalEntityType] = legal_entity_type unless RcsRegistrationInput.omitted?(legal_entity_type)
|
|
815
|
+
body[:organizationType] = organization_type unless RcsRegistrationInput.omitted?(organization_type)
|
|
816
|
+
body[:websiteUrl] = website_url unless RcsRegistrationInput.omitted?(website_url)
|
|
817
|
+
body[:ein] = ein unless RcsRegistrationInput.omitted?(ein)
|
|
818
|
+
body[:stockSymbol] = stock_symbol unless RcsRegistrationInput.omitted?(stock_symbol)
|
|
819
|
+
body[:address] = RcsRegistrationInput.camelize(address) unless RcsRegistrationInput.omitted?(address)
|
|
820
|
+
body[:contact] = RcsRegistrationInput.camelize(contact) unless RcsRegistrationInput.omitted?(contact)
|
|
821
|
+
raise ValidationError, "Provide at least one brand field to update" if body.empty?
|
|
822
|
+
|
|
823
|
+
response = @client.patch("/rcs/brands/#{encoded_id}", body, idempotency_key: idempotency_key)
|
|
824
|
+
RcsBrand.new(response["brand"] || {})
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Draft, edit, test, submit and launch the RCS agents registered for
|
|
829
|
+
# your workspace, and list them.
|
|
139
830
|
class RcsAgentsResource
|
|
140
831
|
def initialize(client)
|
|
141
832
|
@client = client
|
|
142
833
|
end
|
|
143
834
|
|
|
144
|
-
# List your RCS agents, newest first. An empty list means no agent
|
|
145
|
-
#
|
|
835
|
+
# List your RCS agents, newest first. An empty list means no agent has
|
|
836
|
+
# been drafted yet: register one with {#create} or from the dashboard.
|
|
837
|
+
# +stage+ on each agent is its registration stage.
|
|
146
838
|
#
|
|
147
839
|
# @return [Hash] +{ agents: Array<RcsAgent> }+
|
|
148
840
|
#
|
|
@@ -155,16 +847,245 @@ module Sendly
|
|
|
155
847
|
agents = (response["agents"] || []).map { |a| RcsAgent.new(a) }
|
|
156
848
|
{ agents: agents }
|
|
157
849
|
end
|
|
850
|
+
|
|
851
|
+
# Draft an agent under a brand. Everything but +brand_id+ is optional
|
|
852
|
+
# here; required fields are checked when you {#submit}. Nested hashes
|
|
853
|
+
# accept snake_case or camelCase keys. Logo, hero and call-to-action
|
|
854
|
+
# media must already be public https URLs: assets cannot be uploaded
|
|
855
|
+
# over the API, only from the dashboard. Requires the +rcs:write+ scope.
|
|
856
|
+
#
|
|
857
|
+
# @param brand_id [String] The brand this agent belongs to
|
|
858
|
+
# @param display_name [String, nil] The agent name recipients see
|
|
859
|
+
# @param use_case [String, nil] One of {RcsAgentRegistration::USE_CASES}
|
|
860
|
+
# @param basics [Hash, nil] +{ description:, logo_url:, hero_url:, brand_color:,
|
|
861
|
+
# privacy_policy_url:, terms_and_conditions_url:, phone_number: { number:, label: },
|
|
862
|
+
# website: { url:, label: }, email: { address:, label: } }+
|
|
863
|
+
# @param campaign [Hash, nil] +{ company_overview:, agent_overview:, additional_information:,
|
|
864
|
+
# interactions: [{ interaction_type:, description: }], message_examples: [...],
|
|
865
|
+
# consent_settings: { opt_in_methods: [{ method_type:, description: }], call_to_action:,
|
|
866
|
+
# call_to_action_url:, call_to_action_media_url:, double_opt_in:, double_opt_in_message:,
|
|
867
|
+
# opt_in_message:, help_response:, opt_out_response: } }+
|
|
868
|
+
# @param testing [Hash, nil] +{ test_url:, message_id:, additional_information: }+
|
|
869
|
+
# @param idempotency_key [String, nil] Idempotency key for this operation
|
|
870
|
+
# @return [RcsAgentRegistration]
|
|
871
|
+
# @raise [Sendly::NotFoundError] HTTP 404 +rcs_not_found+ when the brand isn't in this workspace
|
|
872
|
+
# @raise [Sendly::ValidationError] HTTP 422 +rcs_invalid_content+ (see +field_errors+)
|
|
873
|
+
#
|
|
874
|
+
# @example
|
|
875
|
+
# agent = client.rcs.agents.create(
|
|
876
|
+
# brand_id: brand.id,
|
|
877
|
+
# display_name: "Acme Coffee",
|
|
878
|
+
# use_case: "MULTI_USE",
|
|
879
|
+
# basics: {
|
|
880
|
+
# description: "Order updates and offers from Acme Coffee",
|
|
881
|
+
# logo_url: "https://acme.example/rcs/logo.png",
|
|
882
|
+
# hero_url: "https://acme.example/rcs/hero.png",
|
|
883
|
+
# brand_color: "#5B3A29",
|
|
884
|
+
# privacy_policy_url: "https://acme.example/privacy",
|
|
885
|
+
# terms_and_conditions_url: "https://acme.example/terms",
|
|
886
|
+
# phone_number: { number: "+15551234567", label: "Support" }
|
|
887
|
+
# }
|
|
888
|
+
# )
|
|
889
|
+
def create(brand_id:, display_name: nil, use_case: nil, basics: nil,
|
|
890
|
+
campaign: nil, testing: nil, idempotency_key: nil)
|
|
891
|
+
raise ValidationError, "brand_id is required" if brand_id.nil? || brand_id.to_s.empty?
|
|
892
|
+
|
|
893
|
+
body = { brandId: brand_id }
|
|
894
|
+
body[:displayName] = display_name unless display_name.nil?
|
|
895
|
+
body[:useCase] = use_case unless use_case.nil?
|
|
896
|
+
body[:basics] = RcsRegistrationInput.camelize(basics) unless basics.nil?
|
|
897
|
+
body[:campaign] = RcsRegistrationInput.camelize(campaign) unless campaign.nil?
|
|
898
|
+
body[:testing] = RcsRegistrationInput.camelize(testing) unless testing.nil?
|
|
899
|
+
|
|
900
|
+
response = @client.post("/rcs/agents", body, idempotency_key: idempotency_key)
|
|
901
|
+
RcsAgentRegistration.new(response["agent"] || {})
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# Fetch one agent's full registration record, including its test
|
|
905
|
+
# devices and review state. Requires the +rcs:read+ scope.
|
|
906
|
+
#
|
|
907
|
+
# @param id [String] Agent identifier
|
|
908
|
+
# @return [RcsAgentRegistration]
|
|
909
|
+
# @raise [Sendly::NotFoundError] HTTP 404 +rcs_not_found+ when the agent isn't in this workspace
|
|
910
|
+
#
|
|
911
|
+
# @example
|
|
912
|
+
# agent = client.rcs.agents.get("rca_abc123")
|
|
913
|
+
# puts agent.customer_stage
|
|
914
|
+
# puts agent.review_note if agent.changes_requested?
|
|
915
|
+
def get(id)
|
|
916
|
+
encoded_id = RcsRegistrationInput.encode_id!(id, "Agent ID")
|
|
917
|
+
response = @client.get("/rcs/agents/#{encoded_id}")
|
|
918
|
+
RcsAgentRegistration.new(response["agent"] || {})
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
# Edit an agent that is still editable. Only the groups you pass are
|
|
922
|
+
# changed: +display_name+, +use_case+ and +basics+ are merged into the
|
|
923
|
+
# basics, +campaign+ and +testing+ are merged section-wise, and
|
|
924
|
+
# +campaign: nil+ or +testing: nil+ clears that section. The same
|
|
925
|
+
# public-https rule applies to logo, hero and call-to-action media.
|
|
926
|
+
# Requires the +rcs:write+ scope.
|
|
927
|
+
#
|
|
928
|
+
# @param id [String] Agent identifier
|
|
929
|
+
# @param display_name [String, nil]
|
|
930
|
+
# @param use_case [String, nil] One of {RcsAgentRegistration::USE_CASES}
|
|
931
|
+
# @param basics [Hash, nil] Partial basics (see {#create})
|
|
932
|
+
# @param campaign [Hash, nil] Partial campaign, or +nil+ to clear it
|
|
933
|
+
# @param testing [Hash, nil] Partial testing details, or +nil+ to clear them
|
|
934
|
+
# @param idempotency_key [String, nil] Idempotency key for this operation
|
|
935
|
+
# @return [RcsAgentRegistration] The updated agent
|
|
936
|
+
# @raise [Sendly::ValidationError] If no field is given, or HTTP 422
|
|
937
|
+
# +rcs_invalid_content+ (see +field_errors+)
|
|
938
|
+
# @raise [Sendly::NotFoundError] HTTP 404 +rcs_not_found+
|
|
939
|
+
# @raise [Sendly::APIError] HTTP 409 +rcs_field_locked+ while the agent is under review
|
|
940
|
+
#
|
|
941
|
+
# @example
|
|
942
|
+
# client.rcs.agents.update(agent.id,
|
|
943
|
+
# campaign: {
|
|
944
|
+
# agent_overview: "Order updates, delivery alerts and support replies",
|
|
945
|
+
# interactions: [{ interaction_type: "TRANSACTIONAL_UPDATES",
|
|
946
|
+
# description: "Order and delivery status" }],
|
|
947
|
+
# message_examples: ["Your order #4821 has shipped!",
|
|
948
|
+
# "Your barista is ready for pickup",
|
|
949
|
+
# "Reply HELP for support or STOP to opt out"]
|
|
950
|
+
# }
|
|
951
|
+
# )
|
|
952
|
+
def update(id, display_name: RcsRegistrationInput::OMIT,
|
|
953
|
+
use_case: RcsRegistrationInput::OMIT,
|
|
954
|
+
basics: RcsRegistrationInput::OMIT,
|
|
955
|
+
campaign: RcsRegistrationInput::OMIT,
|
|
956
|
+
testing: RcsRegistrationInput::OMIT,
|
|
957
|
+
idempotency_key: nil)
|
|
958
|
+
encoded_id = RcsRegistrationInput.encode_id!(id, "Agent ID")
|
|
959
|
+
|
|
960
|
+
body = {}
|
|
961
|
+
body[:displayName] = display_name unless RcsRegistrationInput.omitted?(display_name)
|
|
962
|
+
body[:useCase] = use_case unless RcsRegistrationInput.omitted?(use_case)
|
|
963
|
+
body[:basics] = RcsRegistrationInput.camelize(basics) unless RcsRegistrationInput.omitted?(basics)
|
|
964
|
+
body[:campaign] = RcsRegistrationInput.camelize(campaign) unless RcsRegistrationInput.omitted?(campaign)
|
|
965
|
+
body[:testing] = RcsRegistrationInput.camelize(testing) unless RcsRegistrationInput.omitted?(testing)
|
|
966
|
+
raise ValidationError, "Provide at least one agent field to update" if body.empty?
|
|
967
|
+
|
|
968
|
+
response = @client.patch("/rcs/agents/#{encoded_id}", body, idempotency_key: idempotency_key)
|
|
969
|
+
RcsAgentRegistration.new(response["agent"] || {})
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
# Replace the agent's test devices: the phones that can receive its
|
|
973
|
+
# messages while it is in testing. The list is authoritative, so
|
|
974
|
+
# numbers left out are removed and new ones are invited. Up to 20
|
|
975
|
+
# devices. Requires the +rcs:write+ scope.
|
|
976
|
+
#
|
|
977
|
+
# @param id [String] Agent identifier
|
|
978
|
+
# @param devices [Array<String, Hash>] E.164 numbers, or
|
|
979
|
+
# +{ phone_number:, label: }+ hashes
|
|
980
|
+
# @param idempotency_key [String, nil] Idempotency key for this operation
|
|
981
|
+
# @return [Hash] +{ devices: Array<RcsTestDevice> }+ — the full list after the change
|
|
982
|
+
# @raise [Sendly::ValidationError] If +devices+ isn't an array, or HTTP 422
|
|
983
|
+
# +rcs_invalid_content+ (bad number, more than 20 devices)
|
|
984
|
+
# @raise [Sendly::APIError] HTTP 409 +rcs_field_locked+ while the agent is under review
|
|
985
|
+
#
|
|
986
|
+
# @example
|
|
987
|
+
# client.rcs.agents.set_test_devices(agent.id, devices: [
|
|
988
|
+
# { phone_number: "+15551234567", label: "Sam's Pixel" },
|
|
989
|
+
# "+15557654321"
|
|
990
|
+
# ])
|
|
991
|
+
def set_test_devices(id, devices:, idempotency_key: nil)
|
|
992
|
+
encoded_id = RcsRegistrationInput.encode_id!(id, "Agent ID")
|
|
993
|
+
raise ValidationError, "devices must be an array" unless devices.is_a?(Array)
|
|
994
|
+
|
|
995
|
+
body = {
|
|
996
|
+
devices: devices.map do |device|
|
|
997
|
+
device.is_a?(Hash) ? RcsRegistrationInput.camelize(device) : { phoneNumber: device }
|
|
998
|
+
end
|
|
999
|
+
}
|
|
1000
|
+
response = @client.put("/rcs/agents/#{encoded_id}/test-devices", body,
|
|
1001
|
+
idempotency_key: idempotency_key)
|
|
1002
|
+
list = (response["devices"] || []).map { |d| RcsTestDevice.new(d) }
|
|
1003
|
+
{ devices: list }
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
# Submit the agent and its brand for Sendly's review. Once approved
|
|
1007
|
+
# they go on to the carrier network for brand verification and agent
|
|
1008
|
+
# review; watch +customer_stage+ with {#get}. Requires the +rcs:write+
|
|
1009
|
+
# scope.
|
|
1010
|
+
#
|
|
1011
|
+
# Pass your own +idempotency_key+ if you may retry: a replay returns
|
|
1012
|
+
# the original response without submitting again.
|
|
1013
|
+
#
|
|
1014
|
+
# @param id [String] Agent identifier
|
|
1015
|
+
# @param idempotency_key [String, nil] Idempotency key for this operation
|
|
1016
|
+
# @return [RcsAgentRegistration] The agent, now +awaiting_review?+
|
|
1017
|
+
# @raise [Sendly::ValidationError] HTTP 422 +rcs_invalid_content+ when the
|
|
1018
|
+
# brand or agent basics are incomplete (+field_errors+ lists
|
|
1019
|
+
# +brand.<field>+ / +agent.<field>+ paths)
|
|
1020
|
+
# @raise [Sendly::APIError] HTTP 409 +rcs_field_locked+ when already
|
|
1021
|
+
# submitted, or +rcs_brand_not_verified+ when the brand failed verification
|
|
1022
|
+
#
|
|
1023
|
+
# @example
|
|
1024
|
+
# agent = client.rcs.agents.submit(agent.id, idempotency_key: "rcs-submit-#{agent.id}")
|
|
1025
|
+
# puts agent.review_status # "awaiting_review"
|
|
1026
|
+
def submit(id, idempotency_key: nil)
|
|
1027
|
+
encoded_id = RcsRegistrationInput.encode_id!(id, "Agent ID")
|
|
1028
|
+
response = @client.post("/rcs/agents/#{encoded_id}/submit", {}, idempotency_key: idempotency_key)
|
|
1029
|
+
RcsAgentRegistration.new(response["agent"] || {})
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
# Ask to launch an agent that has been tested on an invited device.
|
|
1033
|
+
# Sendly reviews the campaign and testing details, then sends the
|
|
1034
|
+
# launch on to the carrier network. Requires the +rcs:write+ scope.
|
|
1035
|
+
#
|
|
1036
|
+
# @param id [String] Agent identifier
|
|
1037
|
+
# @param test_url [String, nil] Where the test conversation can be seen; stored
|
|
1038
|
+
# into the agent's testing details first
|
|
1039
|
+
# @param testing_additional_information [String, nil] Anything else the
|
|
1040
|
+
# reviewer should know about the test
|
|
1041
|
+
# @param idempotency_key [String, nil] Idempotency key for this operation
|
|
1042
|
+
# @return [RcsAgentRegistration] The agent, now +launch_requested?+
|
|
1043
|
+
# @raise [Sendly::ValidationError] HTTP 422 +rcs_invalid_content+ when the
|
|
1044
|
+
# campaign or testing details are incomplete (+field_errors+ lists
|
|
1045
|
+
# +campaign.<field>+ / +testing.<field>+ paths)
|
|
1046
|
+
# @raise [Sendly::APIError] HTTP 409 +rcs_launch_not_ready+ until the agent
|
|
1047
|
+
# is in testing, or +rcs_field_locked+ while a request is pending
|
|
1048
|
+
#
|
|
1049
|
+
# @example
|
|
1050
|
+
# client.rcs.agents.request_launch(agent.id,
|
|
1051
|
+
# test_url: "https://acme.example/rcs-test",
|
|
1052
|
+
# testing_additional_information: "Tested on two invited devices"
|
|
1053
|
+
# )
|
|
1054
|
+
def request_launch(id, test_url: nil, testing_additional_information: nil,
|
|
1055
|
+
idempotency_key: nil)
|
|
1056
|
+
encoded_id = RcsRegistrationInput.encode_id!(id, "Agent ID")
|
|
1057
|
+
|
|
1058
|
+
body = {}
|
|
1059
|
+
body[:testUrl] = test_url unless test_url.nil?
|
|
1060
|
+
body[:testingAdditionalInformation] = testing_additional_information unless testing_additional_information.nil?
|
|
1061
|
+
|
|
1062
|
+
response = @client.post("/rcs/agents/#{encoded_id}/request-launch", body,
|
|
1063
|
+
idempotency_key: idempotency_key)
|
|
1064
|
+
RcsAgentRegistration.new(response["agent"] || {})
|
|
1065
|
+
end
|
|
158
1066
|
end
|
|
159
1067
|
|
|
160
|
-
# RCS resource —
|
|
161
|
-
# capability.
|
|
1068
|
+
# RCS resource — register your RCS agent, discover your agents and
|
|
1069
|
+
# pre-flight recipient capability.
|
|
162
1070
|
#
|
|
163
1071
|
# RCS is a first-class Sendly channel: send branded rich messages — text
|
|
164
1072
|
# with suggested replies and actions, or rich cards — via
|
|
165
|
-
# +client.messages.send(channel: "rcs", ...)+.
|
|
166
|
-
#
|
|
167
|
-
#
|
|
1073
|
+
# +client.messages.send(channel: "rcs", ...)+.
|
|
1074
|
+
#
|
|
1075
|
+
# Registration is self-serve, from the dashboard or this API: draft the
|
|
1076
|
+
# brand ({RcsBrandsResource#create}) and the agent
|
|
1077
|
+
# ({RcsAgentsResource#create}), submit them for Sendly's review
|
|
1078
|
+
# ({RcsAgentsResource#submit}), and once approved they go on to the
|
|
1079
|
+
# carrier network for brand verification and agent review. The agent
|
|
1080
|
+
# then reaches invited test devices only
|
|
1081
|
+
# ({RcsAgentsResource#set_test_devices}); when you have tested it,
|
|
1082
|
+
# {RcsAgentsResource#request_launch} asks Sendly to review the campaign
|
|
1083
|
+
# and send the launch on to the carrier network. {RcsRegistrationResource#get}
|
|
1084
|
+
# shows where the registration is, and {RcsDossierResource#get} seeds
|
|
1085
|
+
# the brand from details Sendly already holds. Logo, hero and
|
|
1086
|
+
# call-to-action media must be public https URLs; assets cannot be
|
|
1087
|
+
# uploaded over the API, only from the dashboard. Registration reads
|
|
1088
|
+
# need the +rcs:read+ scope and writes +rcs:write+.
|
|
168
1089
|
#
|
|
169
1090
|
# Delivery is per-recipient: not every device or network supports RCS.
|
|
170
1091
|
# Text messages fall back to plain SMS automatically (billed as SMS)
|
|
@@ -185,12 +1106,24 @@ module Sendly
|
|
|
185
1106
|
# )
|
|
186
1107
|
# end
|
|
187
1108
|
class RcsResource
|
|
188
|
-
# @return [RcsAgentsResource]
|
|
1109
|
+
# @return [RcsAgentsResource] Register and list agents
|
|
189
1110
|
attr_reader :agents
|
|
190
1111
|
|
|
1112
|
+
# @return [RcsRegistrationResource] The registration at a glance
|
|
1113
|
+
attr_reader :registration
|
|
1114
|
+
|
|
1115
|
+
# @return [RcsDossierResource] Business details on file, to seed a brand
|
|
1116
|
+
attr_reader :dossier
|
|
1117
|
+
|
|
1118
|
+
# @return [RcsBrandsResource] Draft and edit brands
|
|
1119
|
+
attr_reader :brands
|
|
1120
|
+
|
|
191
1121
|
def initialize(client)
|
|
192
1122
|
@client = client
|
|
193
1123
|
@agents = RcsAgentsResource.new(client)
|
|
1124
|
+
@registration = RcsRegistrationResource.new(client)
|
|
1125
|
+
@dossier = RcsDossierResource.new(client)
|
|
1126
|
+
@brands = RcsBrandsResource.new(client)
|
|
194
1127
|
end
|
|
195
1128
|
|
|
196
1129
|
# Check whether a recipient can receive RCS.
|
data/lib/sendly/version.rb
CHANGED
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.39.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-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|