sendly 3.37.1 → 3.38.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,655 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sendly
4
+ # A newly started WhatsApp signup. Hand +connect_url+ to a human — they
5
+ # open it in a browser and log in with Facebook to link their WhatsApp
6
+ # Business Account. Poll {WhatsAppSignupResource#get} with +id+ until the
7
+ # status is +"active"+.
8
+ class WhatsAppSignupSession
9
+ attr_reader :id, :connect_url, :status
10
+
11
+ STATUSES = %w[initiated registering active failed expired].freeze
12
+
13
+ def initialize(data)
14
+ @id = data["id"]
15
+ @connect_url = data["connectUrl"] || data["connect_url"]
16
+ @status = data["status"]
17
+ end
18
+
19
+ def active?
20
+ status == "active"
21
+ end
22
+
23
+ def failed?
24
+ status == "failed"
25
+ end
26
+
27
+ def to_h
28
+ { id: id, connect_url: connect_url, status: status }.compact
29
+ end
30
+ end
31
+
32
+ # The status of a WhatsApp signup. +business_account_id+ is nil until the
33
+ # human completes the connect step; +failure_reasons+ is set when the
34
+ # status is +"failed"+.
35
+ class WhatsAppSignup
36
+ attr_reader :id, :status, :phone_number, :business_account_id,
37
+ :failure_reasons, :updated_at
38
+
39
+ STATUSES = %w[initiated registering active failed expired].freeze
40
+
41
+ def initialize(data)
42
+ @id = data["id"]
43
+ @status = data["status"]
44
+ @phone_number = data["phoneNumber"] || data["phone_number"]
45
+ @business_account_id = data["businessAccountId"] || data["business_account_id"]
46
+ @failure_reasons = data["failureReasons"] || data["failure_reasons"]
47
+ @updated_at = data["updatedAt"] || data["updated_at"]
48
+ end
49
+
50
+ def active?
51
+ status == "active"
52
+ end
53
+
54
+ def failed?
55
+ status == "failed"
56
+ end
57
+
58
+ def expired?
59
+ status == "expired"
60
+ end
61
+
62
+ def to_h
63
+ {
64
+ id: id, status: status, phone_number: phone_number,
65
+ business_account_id: business_account_id,
66
+ failure_reasons: failure_reasons, updated_at: updated_at
67
+ }.compact
68
+ end
69
+ end
70
+
71
+ # A number connected (or connecting) to WhatsApp. +display_name+ is the
72
+ # name recipients see — chosen during the connect flow and reviewed by
73
+ # Meta; nil until set. +quality_rating+ is Meta's rating (e.g. "GREEN"),
74
+ # nil before the first rating.
75
+ class WhatsAppSender
76
+ attr_reader :phone_number, :display_name, :status, :quality_rating,
77
+ :created_at
78
+
79
+ STATUSES = %w[pending active suspended].freeze
80
+
81
+ def initialize(data)
82
+ @phone_number = data["phoneNumber"] || data["phone_number"]
83
+ @display_name = data["displayName"] || data["display_name"]
84
+ @status = data["status"]
85
+ @quality_rating = data["qualityRating"] || data["quality_rating"]
86
+ @created_at = data["createdAt"] || data["created_at"]
87
+ end
88
+
89
+ def pending?
90
+ status == "pending"
91
+ end
92
+
93
+ def active?
94
+ status == "active"
95
+ end
96
+
97
+ def suspended?
98
+ status == "suspended"
99
+ end
100
+
101
+ def to_h
102
+ {
103
+ phone_number: phone_number, display_name: display_name,
104
+ status: status, quality_rating: quality_rating, created_at: created_at
105
+ }.compact
106
+ end
107
+ end
108
+
109
+ # A sender's WhatsApp business profile — what recipients see when they
110
+ # open the sender's details in WhatsApp. Unset fields are nil.
111
+ # +profile_photo_url+ is read-only here: the photo cannot be set through
112
+ # {WhatsAppSendersResource#update_profile}.
113
+ class WhatsAppSenderProfile
114
+ attr_reader :phone_number, :display_name, :profile_photo_url, :category,
115
+ :about, :description, :email, :website, :address
116
+
117
+ def initialize(data)
118
+ @phone_number = data["phoneNumber"] || data["phone_number"]
119
+ @display_name = data["displayName"] || data["display_name"]
120
+ @profile_photo_url = data["profilePhotoUrl"] || data["profile_photo_url"]
121
+ @category = data["category"]
122
+ @about = data["about"]
123
+ @description = data["description"]
124
+ @email = data["email"]
125
+ @website = data["website"]
126
+ @address = data["address"]
127
+ end
128
+
129
+ def to_h
130
+ {
131
+ phone_number: phone_number, display_name: display_name,
132
+ profile_photo_url: profile_photo_url, category: category,
133
+ about: about, description: description, email: email,
134
+ website: website, address: address
135
+ }.compact
136
+ end
137
+ end
138
+
139
+ # A WhatsApp message template. Meta reviews every template (usually
140
+ # 24-48h) and may reclassify its category — the category on the record
141
+ # is what drives per-message pricing. +rejection_reason+ is set when the
142
+ # status is +"REJECTED"+; +warnings+ carries non-blocking submission
143
+ # warnings on create responses.
144
+ class WhatsAppTemplate
145
+ attr_reader :id, :name, :language, :category, :status, :quality_rating,
146
+ :rejection_reason, :created_at, :updated_at, :warnings
147
+
148
+ STATUSES = %w[PENDING APPROVED REJECTED PAUSED DISABLED].freeze
149
+ CATEGORIES = %w[AUTHENTICATION UTILITY MARKETING].freeze
150
+
151
+ def initialize(data)
152
+ @id = data["id"]
153
+ @name = data["name"]
154
+ @language = data["language"]
155
+ @category = data["category"]
156
+ @status = data["status"]
157
+ @quality_rating = data["qualityRating"] || data["quality_rating"]
158
+ @rejection_reason = data["rejectionReason"] || data["rejection_reason"]
159
+ @created_at = data["createdAt"] || data["created_at"]
160
+ @updated_at = data["updatedAt"] || data["updated_at"]
161
+ @warnings = data["warnings"]
162
+ end
163
+
164
+ def pending?
165
+ status == "PENDING"
166
+ end
167
+
168
+ def approved?
169
+ status == "APPROVED"
170
+ end
171
+
172
+ def rejected?
173
+ status == "REJECTED"
174
+ end
175
+
176
+ def to_h
177
+ {
178
+ id: id, name: name, language: language, category: category,
179
+ status: status, quality_rating: quality_rating,
180
+ rejection_reason: rejection_reason, created_at: created_at,
181
+ updated_at: updated_at, warnings: warnings
182
+ }.compact
183
+ end
184
+ end
185
+
186
+ # Confirmation that a template was deleted.
187
+ class WhatsAppTemplateDeletion
188
+ attr_reader :id, :deleted
189
+
190
+ def initialize(data)
191
+ @id = data["id"]
192
+ @deleted = data["deleted"] || false
193
+ end
194
+
195
+ def deleted?
196
+ deleted
197
+ end
198
+
199
+ def to_h
200
+ { id: id, deleted: deleted }.compact
201
+ end
202
+ end
203
+
204
+ # Whether a 24-hour customer-service window is open between one of your
205
+ # WhatsApp senders and a recipient. +expires_at+ is when the window
206
+ # closes (ISO 8601), nil when no window is open.
207
+ class WhatsAppWindow
208
+ attr_reader :open, :expires_at
209
+
210
+ def initialize(data)
211
+ @open = data["open"] || false
212
+ @expires_at = data["expiresAt"] || data["expires_at"]
213
+ end
214
+
215
+ def open?
216
+ open
217
+ end
218
+
219
+ def to_h
220
+ { open: open, expires_at: expires_at }.compact
221
+ end
222
+ end
223
+
224
+ # WhatsApp-specific details on a sent message. +kind+ is what was sent
225
+ # ("text", "media", or "template"); +template+ carries the sent
226
+ # template's name, language, and billed category on template sends;
227
+ # +message_id+ is the WhatsApp message id, nil until the first delivery
228
+ # report lands.
229
+ class WhatsAppMessageDetails
230
+ attr_reader :kind, :template, :message_id
231
+
232
+ KINDS = %w[text media template].freeze
233
+
234
+ def initialize(data)
235
+ @kind = data["kind"]
236
+ if data["template"]
237
+ @template = {
238
+ name: data.dig("template", "name"),
239
+ language: data.dig("template", "language"),
240
+ category: data.dig("template", "category")
241
+ }
242
+ end
243
+ @message_id = data["messageId"] || data["message_id"]
244
+ end
245
+
246
+ def to_h
247
+ { kind: kind, template: template, message_id: message_id }.compact
248
+ end
249
+ end
250
+
251
+ # A sent WhatsApp message. Unlike {Message}, +segments+ is always 1
252
+ # (WhatsApp has no segment concept), +text+ is nil for template and media
253
+ # sends, and +whatsapp+ carries the channel-specific details.
254
+ class WhatsAppMessage
255
+ attr_reader :id, :channel, :message_format, :to, :from, :text, :status,
256
+ :segments, :credits_used, :whatsapp, :created_at, :metadata
257
+
258
+ def initialize(data)
259
+ @id = data["id"]
260
+ @channel = data["channel"] || "whatsapp"
261
+ @message_format = data["message_format"] || data["messageFormat"] || "whatsapp"
262
+ @to = data["to"]
263
+ @from = data["from"]
264
+ @text = data["text"]
265
+ @status = data["status"]
266
+ @segments = data["segments"] || 1
267
+ @credits_used = data["creditsUsed"] || data["credits_used"] || 0
268
+ @whatsapp = data["whatsapp"] ? WhatsAppMessageDetails.new(data["whatsapp"]) : nil
269
+ @created_at = data["createdAt"] || data["created_at"]
270
+ @metadata = data["metadata"]
271
+ end
272
+
273
+ def delivered?
274
+ status == "delivered"
275
+ end
276
+
277
+ def failed?
278
+ status == "failed"
279
+ end
280
+
281
+ def to_h
282
+ {
283
+ id: id, channel: channel, message_format: message_format,
284
+ to: to, from: from, text: text, status: status, segments: segments,
285
+ credits_used: credits_used, whatsapp: whatsapp&.to_h,
286
+ created_at: created_at, metadata: metadata
287
+ }.compact
288
+ end
289
+ end
290
+
291
+ # Connect numbers to WhatsApp. Starting a signup returns a +connect_url+
292
+ # a human must complete in a browser.
293
+ class WhatsAppSignupResource
294
+ def initialize(client)
295
+ @client = client
296
+ end
297
+
298
+ # Start connecting a number to WhatsApp.
299
+ #
300
+ # Charges a one-time $19 setup fee (no monthly fee) and returns a
301
+ # +connect_url+. Completing the connection requires a human: hand the
302
+ # URL to your user — they open it in a browser and log in with Facebook
303
+ # to link their WhatsApp Business Account. Then poll {#get} until the
304
+ # status is +"active"+.
305
+ #
306
+ # Calling again for a number with an in-flight signup returns the
307
+ # existing signup (same +connect_url+) without charging again. Requires
308
+ # a live API key.
309
+ #
310
+ # @param phone_number [String] The number to connect, in E.164 format.
311
+ # Must be an active number in your workspace.
312
+ # @return [WhatsAppSignupSession]
313
+ #
314
+ # @example
315
+ # signup = client.whatsapp.signup.create(phone_number: "+15559876543")
316
+ # puts "Open #{signup.connect_url} and log in with Facebook"
317
+ def create(phone_number:)
318
+ raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
319
+
320
+ response = @client.post("/whatsapp/signup", { phoneNumber: phone_number })
321
+ WhatsAppSignupSession.new(response)
322
+ end
323
+
324
+ # Get the status of a WhatsApp signup.
325
+ #
326
+ # @param id [String] The signup's id
327
+ # @return [WhatsAppSignup]
328
+ # @raise [Sendly::NotFoundError] If no such signup exists in your workspace
329
+ #
330
+ # @example
331
+ # signup = client.whatsapp.signup.get("was_xxx")
332
+ # puts signup.failure_reasons if signup.failed?
333
+ def get(id)
334
+ raise ValidationError, "id is required" if id.nil? || id.to_s.empty?
335
+
336
+ encoded_id = URI.encode_www_form_component(id)
337
+ response = @client.get("/whatsapp/signup/#{encoded_id}")
338
+ WhatsAppSignup.new(response)
339
+ end
340
+ end
341
+
342
+ # List the numbers connected (or connecting) to WhatsApp, and manage
343
+ # their business profiles.
344
+ class WhatsAppSendersResource
345
+ def initialize(client)
346
+ @client = client
347
+ end
348
+
349
+ # List your WhatsApp senders, newest first. An empty list means no
350
+ # number is connected yet — start one with
351
+ # {WhatsAppSignupResource#create}.
352
+ #
353
+ # @return [Hash] +{ senders: Array<WhatsAppSender> }+
354
+ #
355
+ # @example
356
+ # client.whatsapp.senders.list[:senders].each do |s|
357
+ # puts "#{s.phone_number} (#{s.display_name || 'no name yet'}) — #{s.status}"
358
+ # end
359
+ def list
360
+ response = @client.get("/whatsapp/senders")
361
+ senders = (response["senders"] || []).map { |s| WhatsAppSender.new(s) }
362
+ { senders: senders }
363
+ end
364
+
365
+ # Get a sender's WhatsApp business profile.
366
+ #
367
+ # The business profile is what recipients see when they open the
368
+ # sender's details in WhatsApp: display name, photo, category, about
369
+ # line, description, and contact details. The sender must be +"active"+
370
+ # (connected to WhatsApp).
371
+ #
372
+ # @param phone_number [String] Your WhatsApp-connected sending number,
373
+ # in E.164 format
374
+ # @return [WhatsAppSenderProfile]
375
+ # @raise [Sendly::NotFoundError] If the number isn't connected to WhatsApp
376
+ #
377
+ # @example
378
+ # profile = client.whatsapp.senders.get_profile("+15559876543")
379
+ # puts profile.display_name
380
+ # puts profile.about
381
+ def get_profile(phone_number)
382
+ raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
383
+
384
+ encoded_number = URI.encode_www_form_component(phone_number)
385
+ response = @client.get("/whatsapp/senders/#{encoded_number}/profile")
386
+ WhatsAppSenderProfile.new(response)
387
+ end
388
+
389
+ # Update a sender's WhatsApp business profile.
390
+ #
391
+ # Pass only the fields to change; omitted fields keep their current
392
+ # value. +about+ is limited to 139 characters and +description+ to 512.
393
+ # +display_name+ changes are reviewed by Meta before they take effect.
394
+ # The profile photo cannot be set through this method. Requires a live
395
+ # API key.
396
+ #
397
+ # @param phone_number [String] Your WhatsApp-connected sending number,
398
+ # in E.164 format
399
+ # @param display_name [String, nil] The business name recipients see
400
+ # @param about [String, nil] Short profile line (max 139 characters)
401
+ # @param description [String, nil] Longer business description (max 512 characters)
402
+ # @param category [String, nil] Business category shown on the profile
403
+ # @param email [String, nil] Contact email shown on the profile
404
+ # @param website [String, nil] Website shown on the profile
405
+ # @param address [String, nil] Business address shown on the profile
406
+ # @return [WhatsAppSenderProfile] The updated profile
407
+ # @raise [Sendly::ValidationError] If no field is given, or a field is too long
408
+ # @raise [Sendly::NotFoundError] If the number isn't connected to WhatsApp
409
+ #
410
+ # @example
411
+ # client.whatsapp.senders.update_profile("+15559876543",
412
+ # about: "Fresh roasted coffee, delivered.",
413
+ # website: "https://acme.example"
414
+ # )
415
+ def update_profile(phone_number, display_name: nil, about: nil, description: nil,
416
+ category: nil, email: nil, website: nil, address: nil)
417
+ raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
418
+
419
+ body = {}
420
+ body[:displayName] = display_name unless display_name.nil?
421
+ body[:about] = about unless about.nil?
422
+ body[:description] = description unless description.nil?
423
+ body[:category] = category unless category.nil?
424
+ body[:email] = email unless email.nil?
425
+ body[:website] = website unless website.nil?
426
+ body[:address] = address unless address.nil?
427
+ raise ValidationError, "Provide at least one profile field to update" if body.empty?
428
+
429
+ encoded_number = URI.encode_www_form_component(phone_number)
430
+ response = @client.patch("/whatsapp/senders/#{encoded_number}/profile", body)
431
+ WhatsAppSenderProfile.new(response)
432
+ end
433
+ end
434
+
435
+ # Manage Meta-reviewed message templates.
436
+ class WhatsAppTemplatesResource
437
+ def initialize(client)
438
+ @client = client
439
+ end
440
+
441
+ # List your WhatsApp templates.
442
+ #
443
+ # @return [Hash] +{ templates: Array<WhatsAppTemplate> }+
444
+ #
445
+ # @example
446
+ # client.whatsapp.templates.list[:templates].each do |t|
447
+ # puts "#{t.name} (#{t.language}) — #{t.status}"
448
+ # end
449
+ def list
450
+ response = @client.get("/whatsapp/templates")
451
+ templates = (response["templates"] || []).map { |t| WhatsAppTemplate.new(t) }
452
+ { templates: templates }
453
+ end
454
+
455
+ # Create a template and submit it to Meta for review.
456
+ #
457
+ # Review usually takes 24-48h; the template is usable once its status
458
+ # is +"APPROVED"+. Requires a live API key.
459
+ #
460
+ # @param sender [String] The WhatsApp-connected sending number this
461
+ # template belongs to, in E.164 format
462
+ # @param name [String] Template name: lowercase letters, digits, and
463
+ # underscores (e.g. "order_shipped")
464
+ # @param language [String] Template language code (e.g. "en_US")
465
+ # @param category [String] "AUTHENTICATION", "UTILITY", or "MARKETING" —
466
+ # drives Meta review rules and pricing
467
+ # @param body [String] Body text. Use {{1}}, {{2}}, ... for variables;
468
+ # every placeholder needs an example value in +examples+
469
+ # @param header [String, nil] Optional text header
470
+ # @param footer [String, nil] Optional footer line
471
+ # @param buttons [Array<Hash>, nil] Optional buttons, each with :type
472
+ # ("url", "quick_reply", or "otp"), :text, and for url buttons a :url
473
+ # (may contain a {{1}} placeholder) plus :example values for review
474
+ # @param examples [Hash, nil] Example values for body placeholders,
475
+ # keyed by placeholder number: { "1" => "Acme Inc", "2" => "#4821" }.
476
+ # Required when the body has variables.
477
+ # @return [WhatsAppTemplate] The created template (status "PENDING"),
478
+ # with any submission warnings
479
+ # @raise [Sendly::NotFoundError] If the sender isn't connected to WhatsApp
480
+ #
481
+ # @example
482
+ # template = client.whatsapp.templates.create(
483
+ # sender: "+15559876543",
484
+ # name: "order_shipped",
485
+ # language: "en_US",
486
+ # category: "UTILITY",
487
+ # body: "Hi {{1}}, your order {{2}} has shipped!",
488
+ # examples: { "1" => "Sam", "2" => "#4821" }
489
+ # )
490
+ # puts template.status # "PENDING"
491
+ def create(sender:, name:, language:, category:, body:,
492
+ header: nil, footer: nil, buttons: nil, examples: nil)
493
+ raise ValidationError, "sender is required" if sender.nil? || sender.to_s.empty?
494
+ raise ValidationError, "name is required" if name.nil? || name.to_s.empty?
495
+ raise ValidationError, "language is required" if language.nil? || language.to_s.empty?
496
+ raise ValidationError, "category is required" if category.nil? || category.to_s.empty?
497
+ raise ValidationError, "body is required" if body.nil? || body.to_s.empty?
498
+
499
+ payload = {
500
+ sender: sender,
501
+ name: name,
502
+ language: language,
503
+ category: category,
504
+ body: body
505
+ }
506
+ payload[:header] = header unless header.nil?
507
+ payload[:footer] = footer unless footer.nil?
508
+ payload[:buttons] = buttons if buttons
509
+ payload[:examples] = examples if examples
510
+
511
+ response = @client.post("/whatsapp/templates", payload)
512
+ WhatsAppTemplate.new(response)
513
+ end
514
+
515
+ # Edit an APPROVED or REJECTED template and resubmit it for review.
516
+ #
517
+ # This is the recovery path for rejections: template names are locked
518
+ # for ~30 days after deletion, so editing a rejected template (rather
519
+ # than deleting and re-creating it) is the way to fix it. The updated
520
+ # template goes back to "PENDING" review. Requires a live API key.
521
+ #
522
+ # @param id [String] The template's id
523
+ # @param body [String, nil] Replacement body text
524
+ # @param header [String, nil] Replacement text header
525
+ # @param footer [String, nil] Replacement footer
526
+ # @param buttons [Array<Hash>, nil] Replacement buttons
527
+ # @param examples [Hash, nil] Replacement example values for body placeholders
528
+ # @return [WhatsAppTemplate] The updated template (status "PENDING")
529
+ # @raise [Sendly::NotFoundError] If no such template exists
530
+ #
531
+ # @example
532
+ # client.whatsapp.templates.update("wat_xxx",
533
+ # body: "Hi {{1}}, your order {{2}} is on its way!",
534
+ # examples: { "1" => "Sam", "2" => "#4821" }
535
+ # )
536
+ def update(id, body: nil, header: nil, footer: nil, buttons: nil, examples: nil)
537
+ raise ValidationError, "id is required" if id.nil? || id.to_s.empty?
538
+
539
+ payload = {}
540
+ payload[:body] = body unless body.nil?
541
+ payload[:header] = header unless header.nil?
542
+ payload[:footer] = footer unless footer.nil?
543
+ payload[:buttons] = buttons unless buttons.nil?
544
+ payload[:examples] = examples unless examples.nil?
545
+
546
+ encoded_id = URI.encode_www_form_component(id)
547
+ response = @client.patch("/whatsapp/templates/#{encoded_id}", payload)
548
+ WhatsAppTemplate.new(response)
549
+ end
550
+
551
+ # Delete a template.
552
+ #
553
+ # Meta locks a deleted template's name for ~30 days — re-creating it
554
+ # fails with +template_name_locked+ until the lock lifts. To fix a
555
+ # rejected template, prefer {#update}. Requires a live API key.
556
+ #
557
+ # @param id [String] The template's id
558
+ # @return [WhatsAppTemplateDeletion]
559
+ # @raise [Sendly::NotFoundError] If no such template exists
560
+ #
561
+ # @example
562
+ # client.whatsapp.templates.delete("wat_xxx")
563
+ def delete(id)
564
+ raise ValidationError, "id is required" if id.nil? || id.to_s.empty?
565
+
566
+ encoded_id = URI.encode_www_form_component(id)
567
+ response = @client.delete("/whatsapp/templates/#{encoded_id}")
568
+ WhatsAppTemplateDeletion.new(response)
569
+ end
570
+ end
571
+
572
+ # WhatsApp resource — connect senders, manage their business profiles and
573
+ # templates, and check 24-hour windows.
574
+ #
575
+ # WhatsApp is a first-class Sendly channel: connect a number you own,
576
+ # create Meta-reviewed message templates, and send via
577
+ # +client.messages.send(channel: "whatsapp", ...)+.
578
+ #
579
+ # Connecting a number is a one-time $19 setup (no monthly fee) and always
580
+ # ends with a human step: {WhatsAppSignupResource#create} returns a
581
+ # +connect_url+ that a person must open in a browser and log in with
582
+ # Facebook to link their WhatsApp Business Account.
583
+ #
584
+ # Two ways to reach a recipient:
585
+ #
586
+ # - Inside a 24-hour window (the recipient messaged you in the last 24h):
587
+ # free-form text and media are allowed. Check with {#window}.
588
+ # - Anytime: an approved template. Templates are reviewed by Meta
589
+ # (typically 24-48h) and categorized as authentication, utility, or
590
+ # marketing — pricing follows the category and destination country.
591
+ # Note: Meta has paused marketing template delivery to US (+1) numbers.
592
+ #
593
+ # @example Connect a number, create a template, then send
594
+ # # 1. Connect ($19 one-time; a human must open the connect URL)
595
+ # signup = client.whatsapp.signup.create(phone_number: "+15559876543")
596
+ # puts "Have your user open: #{signup.connect_url}"
597
+ #
598
+ # # 2. Poll until active
599
+ # status = client.whatsapp.signup.get(signup.id)
600
+ #
601
+ # # 3. Create a template (Meta reviews it, usually 24-48h)
602
+ # client.whatsapp.templates.create(
603
+ # sender: "+15559876543",
604
+ # name: "order_shipped",
605
+ # language: "en_US",
606
+ # category: "UTILITY",
607
+ # body: "Hi {{1}}, your order {{2}} has shipped!",
608
+ # examples: { "1" => "Sam", "2" => "#4821" }
609
+ # )
610
+ #
611
+ # # 4. Send — free-form inside an open 24h window, template anytime
612
+ # window = client.whatsapp.window(from: "+15559876543", to: "+15551234567")
613
+ class WhatsAppResource
614
+ # @return [WhatsAppSignupResource] Connect numbers to WhatsApp
615
+ attr_reader :signup
616
+
617
+ # @return [WhatsAppSendersResource] List connected numbers and manage
618
+ # their business profiles
619
+ attr_reader :senders
620
+
621
+ # @return [WhatsAppTemplatesResource] Manage Meta-reviewed templates
622
+ attr_reader :templates
623
+
624
+ def initialize(client)
625
+ @client = client
626
+ @signup = WhatsAppSignupResource.new(client)
627
+ @senders = WhatsAppSendersResource.new(client)
628
+ @templates = WhatsAppTemplatesResource.new(client)
629
+ end
630
+
631
+ # Check whether a 24-hour customer-service window is open between one
632
+ # of your WhatsApp senders and a recipient.
633
+ #
634
+ # Free-form text and media only deliver while a window is open (it
635
+ # opens when the recipient messages you and lasts 24h from their last
636
+ # inbound message). Outside a window, send an approved template.
637
+ #
638
+ # @param from [String] Your WhatsApp-connected sending number, in E.164 format
639
+ # @param to [String] The recipient's number, in E.164 format
640
+ # @return [WhatsAppWindow]
641
+ #
642
+ # @example
643
+ # window = client.whatsapp.window(from: "+15559876543", to: "+15551234567")
644
+ # unless window.open?
645
+ # # Use a template send instead of free-form text
646
+ # end
647
+ def window(from:, to:)
648
+ raise ValidationError, "from is required" if from.nil? || from.to_s.empty?
649
+ raise ValidationError, "to is required" if to.nil? || to.to_s.empty?
650
+
651
+ response = @client.get("/whatsapp/window", { from: from, to: to })
652
+ WhatsAppWindow.new(response)
653
+ end
654
+ end
655
+ end
data/lib/sendly.rb CHANGED
@@ -25,6 +25,8 @@ require_relative "sendly/business_upgrade_resource"
25
25
  require_relative "sendly/numbers_resource"
26
26
  require_relative "sendly/tendlc_resource"
27
27
  require_relative "sendly/links_resource"
28
+ require_relative "sendly/whatsapp_resource"
29
+ require_relative "sendly/rcs_resource"
28
30
 
29
31
  # Sendly Ruby SDK
30
32
  #