sendly 3.37.1 → 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.
@@ -0,0 +1,1155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sendly
4
+ # An RCS agent registered for your workspace — the branded identity your
5
+ # RCS messages come from. +status+ moves +"draft"+ → +"submitted"+ →
6
+ # +"testing"+ (can send, but only to invited test devices) → +"approved"+
7
+ # (can send to everyone); +"suspended"+ agents cannot send. +sendable+ is
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}.
11
+ class RcsAgent
12
+ attr_reader :id, :name, :status, :use_case, :sendable, :stage, :created_at
13
+
14
+ STATUSES = %w[draft submitted testing approved suspended].freeze
15
+
16
+ def initialize(data)
17
+ @id = data["id"]
18
+ @name = data["name"]
19
+ @status = data["status"]
20
+ @use_case = data["useCase"] || data["use_case"]
21
+ @sendable = data["sendable"] || false
22
+ @stage = data["stage"]
23
+ @created_at = data["createdAt"] || data["created_at"]
24
+ end
25
+
26
+ def sendable?
27
+ sendable
28
+ end
29
+
30
+ def approved?
31
+ status == "approved"
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ id: id, name: name, status: status, use_case: use_case,
37
+ sendable: sendable, stage: stage, created_at: created_at
38
+ }.compact
39
+ end
40
+ end
41
+
42
+ # Whether a recipient's device and network can receive RCS. When
43
+ # +capable+ is false, a text send to this recipient falls back to plain
44
+ # SMS (unless the fallback is disabled) and a card send fails. +features+
45
+ # lists the feature tags the device reports.
46
+ class RcsCapability
47
+ attr_reader :to, :agent_id, :capable, :features
48
+
49
+ def initialize(data)
50
+ @to = data["to"]
51
+ @agent_id = data["agentId"] || data["agent_id"]
52
+ @capable = data["capable"] || false
53
+ @features = data["features"] || []
54
+ end
55
+
56
+ def capable?
57
+ capable
58
+ end
59
+
60
+ def to_h
61
+ { to: to, agent_id: agent_id, capable: capable, features: features }.compact
62
+ end
63
+ end
64
+
65
+ # RCS-specific details on a sent message. On a native RCS delivery +kind+
66
+ # is what was sent ("text" or "card") and +agent_name+ is the brand name
67
+ # recipients see. On an SMS fallback +requested_channel+ is "rcs" and
68
+ # +suggestions_dropped+ is true when suggested replies/actions were
69
+ # dropped (they have no SMS form).
70
+ class RcsMessageDetails
71
+ attr_reader :kind, :agent_id, :agent_name, :requested_channel,
72
+ :suggestions_dropped
73
+
74
+ KINDS = %w[text card].freeze
75
+
76
+ def initialize(data)
77
+ @kind = data["kind"]
78
+ @agent_id = data["agentId"] || data["agent_id"]
79
+ @agent_name = data["agentName"] || data["agent_name"]
80
+ @requested_channel = data["requestedChannel"] || data["requested_channel"]
81
+ @suggestions_dropped = data["suggestionsDropped"] || data["suggestions_dropped"] || false
82
+ end
83
+
84
+ def to_h
85
+ {
86
+ kind: kind, agent_id: agent_id, agent_name: agent_name,
87
+ requested_channel: requested_channel,
88
+ suggestions_dropped: suggestions_dropped
89
+ }.compact
90
+ end
91
+ end
92
+
93
+ # A message sent through the RCS channel. +channel+ is "rcs" when it was
94
+ # delivered as RCS, or "sms" when the recipient couldn't receive RCS and
95
+ # the message fell back to plain SMS — check with {#fell_back?}. On a
96
+ # fallback +fell_back_to+ is "sms" and +segments+/+credits_used+ are
97
+ # billed as SMS; +rcs+ carries the channel-specific details either way.
98
+ class RcsMessage
99
+ attr_reader :id, :channel, :fell_back_to, :message_format, :to, :from,
100
+ :text, :status, :segments, :credits_used, :rcs, :created_at,
101
+ :metadata
102
+
103
+ def initialize(data)
104
+ @id = data["id"]
105
+ @channel = data["channel"] || "rcs"
106
+ @fell_back_to = data["fellBackTo"] || data["fell_back_to"]
107
+ @message_format = data["message_format"] || data["messageFormat"] || @channel
108
+ @to = data["to"]
109
+ @from = data["from"]
110
+ @text = data["text"]
111
+ @status = data["status"]
112
+ @segments = data["segments"] || 1
113
+ @credits_used = data["creditsUsed"] || data["credits_used"] || 0
114
+ @rcs = data["rcs"] ? RcsMessageDetails.new(data["rcs"]) : nil
115
+ @created_at = data["createdAt"] || data["created_at"]
116
+ @metadata = data["metadata"]
117
+ end
118
+
119
+ def fell_back?
120
+ fell_back_to == "sms"
121
+ end
122
+
123
+ def delivered?
124
+ status == "delivered"
125
+ end
126
+
127
+ def failed?
128
+ status == "failed"
129
+ end
130
+
131
+ def to_h
132
+ {
133
+ id: id, channel: channel, fell_back_to: fell_back_to,
134
+ message_format: message_format, to: to, from: from, text: text,
135
+ status: status, segments: segments, credits_used: credits_used,
136
+ rcs: rcs&.to_h, created_at: created_at, metadata: metadata
137
+ }.compact
138
+ end
139
+ end
140
+
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.
830
+ class RcsAgentsResource
831
+ def initialize(client)
832
+ @client = client
833
+ end
834
+
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.
838
+ #
839
+ # @return [Hash] +{ agents: Array<RcsAgent> }+
840
+ #
841
+ # @example
842
+ # client.rcs.agents.list[:agents].each do |a|
843
+ # puts "#{a.name} — #{a.status}#{a.sendable? ? ' (sendable)' : ''}"
844
+ # end
845
+ def list
846
+ response = @client.get("/rcs/agents")
847
+ agents = (response["agents"] || []).map { |a| RcsAgent.new(a) }
848
+ { agents: agents }
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
1066
+ end
1067
+
1068
+ # RCS resource — register your RCS agent, discover your agents and
1069
+ # pre-flight recipient capability.
1070
+ #
1071
+ # RCS is a first-class Sendly channel: send branded rich messages — text
1072
+ # with suggested replies and actions, or rich cards — via
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+.
1089
+ #
1090
+ # Delivery is per-recipient: not every device or network supports RCS.
1091
+ # Text messages fall back to plain SMS automatically (billed as SMS)
1092
+ # unless you pass +fallback_to_sms: false+; rich cards have no SMS form
1093
+ # and only deliver to RCS-capable recipients. Use {#capability} to check
1094
+ # a recipient before sending.
1095
+ #
1096
+ # RCS sends and capability checks require a live API key
1097
+ # (+sk_live_v1_xxx+).
1098
+ #
1099
+ # @example Check capability, then send
1100
+ # cap = client.rcs.capability(to: "+15551234567")
1101
+ # if cap.capable?
1102
+ # client.messages.send(
1103
+ # channel: "rcs",
1104
+ # to: "+15551234567",
1105
+ # text: "Your order has shipped!"
1106
+ # )
1107
+ # end
1108
+ class RcsResource
1109
+ # @return [RcsAgentsResource] Register and list agents
1110
+ attr_reader :agents
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
+
1121
+ def initialize(client)
1122
+ @client = client
1123
+ @agents = RcsAgentsResource.new(client)
1124
+ @registration = RcsRegistrationResource.new(client)
1125
+ @dossier = RcsDossierResource.new(client)
1126
+ @brands = RcsBrandsResource.new(client)
1127
+ end
1128
+
1129
+ # Check whether a recipient can receive RCS.
1130
+ #
1131
+ # Probes the recipient's device and network. When the recipient is not
1132
+ # capable, a text send falls back to plain SMS (unless the fallback is
1133
+ # disabled) and a card send fails with
1134
+ # +rcs_not_supported_for_recipient+.
1135
+ #
1136
+ # @param to [String] The recipient's number, in E.164 format
1137
+ # @param agent_id [String, nil] The agent to probe with. Optional when
1138
+ # your workspace has exactly one sendable agent; required (the API
1139
+ # responds 400 +rcs_agent_ambiguous+) when it has more.
1140
+ # @return [RcsCapability]
1141
+ #
1142
+ # @example
1143
+ # cap = client.rcs.capability(to: "+15551234567")
1144
+ # puts cap.capable?
1145
+ # puts cap.features.inspect
1146
+ def capability(to:, agent_id: nil)
1147
+ raise ValidationError, "to is required" if to.nil? || to.to_s.empty?
1148
+
1149
+ params = { to: to }
1150
+ params[:agentId] = agent_id if agent_id
1151
+ response = @client.get("/rcs/capability", params)
1152
+ RcsCapability.new(response)
1153
+ end
1154
+ end
1155
+ end