ruby-whatsapp 0.4.0 → 0.4.2

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +276 -634
  4. data/docs/README.md +73 -0
  5. data/docs/business_phone_number/README.md +232 -0
  6. data/docs/business_phone_number/account.md +170 -0
  7. data/docs/business_phone_number/profile.md +220 -0
  8. data/docs/configuration.md +172 -0
  9. data/docs/errors.md +208 -0
  10. data/docs/media/README.md +161 -0
  11. data/docs/message_templates/README.md +206 -0
  12. data/docs/message_templates/authentication.md +107 -0
  13. data/docs/message_templates/carousel.md +103 -0
  14. data/docs/message_templates/components.md +179 -0
  15. data/docs/message_templates/library.md +98 -0
  16. data/docs/message_templates/limited_time_offer.md +94 -0
  17. data/docs/message_templates/responses.md +173 -0
  18. data/docs/message_templates/standard.md +138 -0
  19. data/docs/messages/README.md +135 -0
  20. data/docs/messages/address.md +86 -0
  21. data/docs/messages/audio.md +43 -0
  22. data/docs/messages/contacts.md +129 -0
  23. data/docs/messages/document.md +49 -0
  24. data/docs/messages/image.md +59 -0
  25. data/docs/messages/interactive.md +281 -0
  26. data/docs/messages/location.md +51 -0
  27. data/docs/messages/location_request.md +59 -0
  28. data/docs/messages/mark_message_as_read.md +84 -0
  29. data/docs/messages/reaction.md +59 -0
  30. data/docs/messages/sticker.md +40 -0
  31. data/docs/messages/template.md +166 -0
  32. data/docs/messages/text.md +55 -0
  33. data/docs/messages/video.md +41 -0
  34. data/docs/subscribed_app/README.md +139 -0
  35. data/docs/webhooks/README.md +209 -0
  36. data/docs/webhooks/account_alerts.md +44 -0
  37. data/docs/webhooks/account_review_update.md +29 -0
  38. data/docs/webhooks/account_update.md +54 -0
  39. data/docs/webhooks/automatic_events.md +47 -0
  40. data/docs/webhooks/business_capability_update.md +40 -0
  41. data/docs/webhooks/history.md +44 -0
  42. data/docs/webhooks/message_template_components_update.md +46 -0
  43. data/docs/webhooks/message_template_quality_update.md +49 -0
  44. data/docs/webhooks/message_template_status_update.md +55 -0
  45. data/docs/webhooks/messages.md +385 -0
  46. data/docs/webhooks/partner_solutions.md +35 -0
  47. data/docs/webhooks/payment_configuration_update.md +40 -0
  48. data/docs/webhooks/phone_number_name_update.md +44 -0
  49. data/docs/webhooks/phone_number_quality_update.md +40 -0
  50. data/docs/webhooks/security.md +43 -0
  51. data/docs/webhooks/smb_app_state_sync.md +53 -0
  52. data/docs/webhooks/smb_message_echoes.md +54 -0
  53. data/docs/webhooks/template_category_update.md +52 -0
  54. data/docs/webhooks/user_preferences.md +48 -0
  55. data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
  56. data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
  57. data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
  58. data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
  59. data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
  60. data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
  61. data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
  62. data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
  63. data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
  64. data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
  65. data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
  66. data/lib/ruby/whatsapp/business_phone_number/transport.rb +11 -12
  67. data/lib/ruby/whatsapp/business_phone_number.rb +11 -4
  68. data/lib/ruby/whatsapp/message_templates.rb +2 -6
  69. data/lib/ruby/whatsapp/path_building.rb +33 -0
  70. data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
  71. data/lib/ruby/whatsapp/version.rb +1 -1
  72. metadata +63 -1
@@ -0,0 +1,52 @@
1
+ # `template_category_update`
2
+
3
+ Meta reclassified a template — most often moving something you filed as `UTILITY` into
4
+ `MARKETING`. This matters because category determines **how you are billed** and
5
+ whether the message is subject to marketing opt-outs.
6
+
7
+ `Whatsapp::Webhook::TemplateCategoryUpdate` · confidence: **moderate-high**
8
+
9
+ ## Payload
10
+
11
+ ```json
12
+ { "field": "template_category_update",
13
+ "value": {
14
+ "message_template_id": "123",
15
+ "message_template_name": "order_confirmation",
16
+ "message_template_language": "en_US",
17
+ "previous_category": "MARKETING",
18
+ "new_category": "UTILITY",
19
+ "correct_category": "UTILITY"
20
+ } }
21
+ ```
22
+
23
+ ## Accessors
24
+
25
+ | Accessor | Meaning |
26
+ | --- | --- |
27
+ | `message_template_id` | The template's ID |
28
+ | `message_template_name` | Its name |
29
+ | `message_template_language` | Its locale |
30
+ | `previous_category` | What it was |
31
+ | `new_category` | What it is now |
32
+ | `correct_category` | What Meta believes it should be |
33
+
34
+ ## Handling it
35
+
36
+ ```ruby
37
+ when "template_category_update"
38
+ update = change.value
39
+
40
+ MessageTemplate
41
+ .find_by(meta_id: update.message_template_id)
42
+ &.update!(category: update.new_category)
43
+
44
+ Billing.recheck!(update.message_template_name) if update.new_category == "MARKETING"
45
+ ```
46
+
47
+ > **Best-effort schema.** Meta publishes no JSON example for this field. Validate
48
+ > against a real payload before depending on it in production.
49
+
50
+ ---
51
+
52
+ **Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
@@ -0,0 +1,48 @@
1
+ # `user_preferences`
2
+
3
+ A customer changed their marketing preferences — most importantly, opting **out**.
4
+ Respecting this is not optional: continuing to send marketing to someone who stopped
5
+ you damages your quality rating and breaches Meta's policy.
6
+
7
+ `Whatsapp::Webhook::UserPreferences` · confidence: **moderate**
8
+
9
+ ## Payload
10
+
11
+ ```json
12
+ { "field": "user_preferences",
13
+ "value": { "wa_id": "16505551234",
14
+ "detail": "stopped marketing messages",
15
+ "value": "stop",
16
+ "timestamp": "1750263773" } }
17
+ ```
18
+
19
+ ## Accessors
20
+
21
+ | Accessor | Meaning |
22
+ | --- | --- |
23
+ | `wa_id` | The customer's WhatsApp ID |
24
+ | `detail` | Human-readable description |
25
+ | `value` | `"stop"` or `"resume"` |
26
+ | `timestamp` | Unix seconds, as a String |
27
+
28
+ ## Handling it
29
+
30
+ ```ruby
31
+ when "user_preferences"
32
+ pref = change.value
33
+
34
+ Customer
35
+ .find_by(wa_id: pref.wa_id)
36
+ &.update!(marketing_opt_in: pref.value != "stop",
37
+ marketing_opt_in_changed_at: Time.at(pref.timestamp.to_i))
38
+ ```
39
+
40
+ Gate every `MARKETING`-category [template send](../messages/template.md) on this flag.
41
+ `UTILITY` and `AUTHENTICATION` templates are unaffected.
42
+
43
+ > **Best-effort schema.** Meta publishes no JSON example for this field. Validate
44
+ > against a real payload before depending on it in production.
45
+
46
+ ---
47
+
48
+ **Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module Account
6
+ # The WhatsApp Business Account node returned by {Get}.
7
+ #
8
+ # Only `id` and `name` are documented required; everything else arrives just when
9
+ # asked for via `fields`, so every attribute tolerates being absent. Status values
10
+ # are exposed as raw strings and are never validated on the way in — this is
11
+ # read-side data, and a value Meta adds later must still round-trip. The frozen
12
+ # constant modules below are for comparison, not enforcement, mirroring how
13
+ # {Whatsapp::MessageTemplates::Response::Node} keeps Meta's echoed payloads raw.
14
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
15
+ class Details
16
+ # Meta's review status for the account.
17
+ module ReviewStatuses
18
+ APPROVED = "APPROVED"
19
+ DEFERRED = "DEFERRED"
20
+ PENDING = "PENDING"
21
+ REJECTED = "REJECTED"
22
+
23
+ ALL = [APPROVED, DEFERRED, PENDING, REJECTED].freeze
24
+ end
25
+
26
+ # Where the account sits in Meta's business verification process.
27
+ module VerificationStatuses
28
+ EXPIRED = "EXPIRED"
29
+ FAILED = "FAILED"
30
+ INELIGIBLE = "INELIGIBLE"
31
+ NOT_VERIFIED = "NOT_VERIFIED"
32
+ PENDING = "PENDING"
33
+ PENDING_NEED_MORE_INFO = "PENDING_NEED_MORE_INFO"
34
+ PENDING_SUBMISSION = "PENDING_SUBMISSION"
35
+ REJECTED = "REJECTED"
36
+ REVOKED = "REVOKED"
37
+ VERIFIED = "VERIFIED"
38
+
39
+ ALL = [EXPIRED, FAILED, INELIGIBLE, NOT_VERIFIED, PENDING, PENDING_NEED_MORE_INFO,
40
+ PENDING_SUBMISSION, REJECTED, REVOKED, VERIFIED,].freeze
41
+ end
42
+
43
+ # Who owns the account: your own business, a client's, or a partner acting for one.
44
+ module OwnershipTypes
45
+ CLIENT_OWNED = "CLIENT_OWNED"
46
+ ON_BEHALF_OF = "ON_BEHALF_OF"
47
+ SELF = "SELF"
48
+
49
+ ALL = [CLIENT_OWNED, ON_BEHALF_OF, SELF].freeze
50
+ end
51
+
52
+ # @!attribute [rw] id
53
+ # @return [String, nil] The account's unique identifier.
54
+ attr_accessor :id
55
+
56
+ # @!attribute [rw] name
57
+ # @return [String, nil] The account's human-readable name.
58
+ attr_accessor :name
59
+
60
+ # @!attribute [rw] timezone_id
61
+ # @return [String, nil] The account's timezone identifier.
62
+ attr_accessor :timezone_id
63
+
64
+ # @!attribute [rw] message_template_namespace
65
+ # @return [String, nil] The namespace the account's templates live in.
66
+ attr_accessor :message_template_namespace
67
+
68
+ # @!attribute [rw] account_review_status
69
+ # @return [String, nil] One of {ReviewStatuses::ALL}.
70
+ attr_accessor :account_review_status
71
+
72
+ # @!attribute [rw] business_verification_status
73
+ # @return [String, nil] One of {VerificationStatuses::ALL}.
74
+ attr_accessor :business_verification_status
75
+
76
+ # @!attribute [rw] country
77
+ # @return [String, nil] The account's country code.
78
+ attr_accessor :country
79
+
80
+ # @!attribute [rw] ownership_type
81
+ # @return [String, nil] One of {OwnershipTypes::ALL}.
82
+ attr_accessor :ownership_type
83
+
84
+ # @!attribute [rw] primary_business_location
85
+ # @return [String, nil] The account's primary business location.
86
+ attr_accessor :primary_business_location
87
+
88
+ # @param id [String, nil]
89
+ # @param name [String, nil]
90
+ # @param timezone_id [String, nil]
91
+ # @param message_template_namespace [String, nil]
92
+ # @param account_review_status [String, nil]
93
+ # @param business_verification_status [String, nil]
94
+ # @param country [String, nil]
95
+ # @param ownership_type [String, nil]
96
+ # @param primary_business_location [String, nil]
97
+ def initialize(id: nil, name: nil, timezone_id: nil, message_template_namespace: nil,
98
+ account_review_status: nil, business_verification_status: nil, country: nil,
99
+ ownership_type: nil, primary_business_location: nil)
100
+ @id = id
101
+ @name = name
102
+ @timezone_id = timezone_id
103
+ @message_template_namespace = message_template_namespace
104
+ @account_review_status = account_review_status
105
+ @business_verification_status = business_verification_status
106
+ @country = country
107
+ @ownership_type = ownership_type
108
+ @primary_business_location = primary_business_location
109
+ end
110
+
111
+ class << self
112
+ # @param response [Hash, nil] The parsed response body.
113
+ # @return [Details]
114
+ def deserialize(response)
115
+ response ||= {}
116
+
117
+ new(
118
+ id: response["id"],
119
+ name: response["name"],
120
+ timezone_id: response["timezone_id"],
121
+ message_template_namespace: response["message_template_namespace"],
122
+ account_review_status: response["account_review_status"],
123
+ business_verification_status: response["business_verification_status"],
124
+ country: response["country"],
125
+ ownership_type: response["ownership_type"],
126
+ primary_business_location: response["primary_business_location"]
127
+ )
128
+ end
129
+ end
130
+
131
+ # @return [Boolean] Whether Meta has approved the account.
132
+ def approved?
133
+ account_review_status == ReviewStatuses::APPROVED
134
+ end
135
+
136
+ # @return [Boolean] Whether the business behind the account is verified.
137
+ def verified?
138
+ business_verification_status == VerificationStatuses::VERIFIED
139
+ end
140
+
141
+ # @return [Boolean] Whether the account belongs to your own business, rather
142
+ # than to a client or a partner acting on one's behalf.
143
+ def self_owned?
144
+ ownership_type == OwnershipTypes::SELF
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module Account
6
+ # Reads a WhatsApp Business Account's details: its configuration, review and
7
+ # verification status, and ownership.
8
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
9
+ class Get
10
+ extend ResponseHandling
11
+ extend Transport
12
+
13
+ # The fields Meta documents for this node. Offered for convenience, not
14
+ # enforcement — {.call} deliberately does not validate `fields` against this
15
+ # list (nor does {Whatsapp::SubscribedApp::List}), so a field Meta adds later
16
+ # works without a gem release.
17
+ module Fields
18
+ ID = "id"
19
+ NAME = "name"
20
+ TIMEZONE_ID = "timezone_id"
21
+ MESSAGE_TEMPLATE_NAMESPACE = "message_template_namespace"
22
+ ACCOUNT_REVIEW_STATUS = "account_review_status"
23
+ BUSINESS_VERIFICATION_STATUS = "business_verification_status"
24
+ COUNTRY = "country"
25
+ OWNERSHIP_TYPE = "ownership_type"
26
+ PRIMARY_BUSINESS_LOCATION = "primary_business_location"
27
+
28
+ ALL = [ID, NAME, TIMEZONE_ID, MESSAGE_TEMPLATE_NAMESPACE, ACCOUNT_REVIEW_STATUS,
29
+ BUSINESS_VERIFICATION_STATUS, COUNTRY, OWNERSHIP_TYPE, PRIMARY_BUSINESS_LOCATION,].freeze
30
+ end
31
+
32
+ class << self
33
+ # @example Read everything Meta publishes for the account
34
+ # Get.call(fields: Get::Fields::ALL)
35
+ # @param client [Whatsapp::Client] The WhatsApp client instance.
36
+ # @param fields [Array<String>, String, nil] Restrict the response to these
37
+ # fields; see {Fields::ALL}. Omitted entirely when nil, in which case Meta
38
+ # returns only its own defaults (`id` and `name`).
39
+ # @return [Details]
40
+ # @raise [Error] if no WABA ID is configured, or the request fails.
41
+ def call(client: Client.new, fields: nil)
42
+ params = fields ? { fields: Array(fields).join(",") } : {}
43
+ response = client.connection.get(node_path(client), params:)
44
+
45
+ Details.deserialize(
46
+ parse_json(handle_response!(response, error_class: Error, action: "get business account details"))
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module Account
6
+ # The one piece shared by {Get} and {Update}: both address the WABA node itself,
7
+ # differing only in HTTP verb and body. Extended (not included) since those
8
+ # classes are class-method-only.
9
+ #
10
+ # Deliberately separate from {BusinessPhoneNumber::Transport}, which this shadows
11
+ # by lexical scope inside {Account}. That one guards `phone_id` and always appends
12
+ # an edge segment; this addresses `waba_id` and no edge at all. Both delegate the
13
+ # guard and its wording to {Whatsapp::PathBuilding}, so the method names are now the
14
+ # whole of the difference — `node_path` here, `edge_path` there, so the two can never
15
+ # be confused at a call site.
16
+ module Transport
17
+ include Whatsapp::PathBuilding
18
+
19
+ private
20
+
21
+ # @param client [Whatsapp::Client]
22
+ # @return [String] The versioned path to the WABA node, with no edge segment.
23
+ # @raise [Error] if no WABA ID is configured.
24
+ def node_path(client)
25
+ scoped_path(client, :waba_id, error_class: Error, purpose: "business account details")
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module Account
6
+ # Updates a WhatsApp Business Account's name or timezone.
7
+ #
8
+ # A validated instance rather than a bare class method (unlike {Get}, which has
9
+ # nothing to check): Meta documents `name` as a non-empty string, and since both
10
+ # fields are optional an all-nil call would spend a request to change nothing.
11
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
12
+ class Update
13
+ include ActiveModel::Validations
14
+ extend ResponseHandling
15
+ extend Transport
16
+
17
+ # @!attribute [rw] name
18
+ # @return [String, nil] The account's new name.
19
+ attr_accessor :name
20
+
21
+ # @!attribute [rw] timezone_id
22
+ # @return [String, nil] The account's new timezone identifier.
23
+ attr_accessor :timezone_id
24
+
25
+ validate :validate_any_attribute_present
26
+ validates :name, presence: true, allow_nil: true
27
+ validates :timezone_id, presence: true, allow_nil: true
28
+
29
+ # @param name [String, nil] The account's new name. Meta rejects an empty
30
+ # string, so a blank value fails here rather than at the API.
31
+ # @param timezone_id [String, nil] The account's new timezone identifier. Meta
32
+ # publishes no enum for this field, so only presence is checked.
33
+ # @raise [ActiveModel::ValidationError] if both are nil, or either is blank.
34
+ def initialize(name: nil, timezone_id: nil)
35
+ @name = name
36
+ @timezone_id = timezone_id
37
+
38
+ validate!
39
+ end
40
+
41
+ # @return [Hash] The update payload. Both fields are optional, so an omitted
42
+ # one is compacted away rather than sent as null — which Meta would read as
43
+ # an instruction to blank it.
44
+ def serialize
45
+ { name:, timezone_id: }.compact
46
+ end
47
+
48
+ class << self
49
+ # @param client [Whatsapp::Client] The WhatsApp client instance.
50
+ # @param name [String, nil] See {#initialize}.
51
+ # @param timezone_id [String, nil] See {#initialize}.
52
+ # @return [BusinessPhoneNumber::Response]
53
+ # @raise [ActiveModel::ValidationError] if no attribute is given, or one is blank.
54
+ # @raise [Error] if no WABA ID is configured, or the request fails.
55
+ def call(client: Client.new, name: nil, timezone_id: nil)
56
+ body = new(name:, timezone_id:).serialize
57
+ response = client.connection.post(node_path(client), json: body)
58
+
59
+ BusinessPhoneNumber::Response.deserialize(
60
+ parse_json(handle_response!(response, error_class: Error, action: "update business account"))
61
+ )
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ # @return [void]
68
+ def validate_any_attribute_present
69
+ return unless name.nil? && timezone_id.nil?
70
+
71
+ errors.add(:base, "at least one of name or timezone_id must be provided")
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ # Reads and updates the WhatsApp Business Account node itself — the account a
6
+ # business phone number belongs to, as opposed to the number's own onboarding
7
+ # state that the rest of this module drives.
8
+ #
9
+ # This is the one part of {Whatsapp::BusinessPhoneNumber} that addresses `waba_id`
10
+ # rather than `phone_id`, so it carries its own {Transport} and cannot share
11
+ # {BusinessPhoneNumber::Transport}. It is also the only endpoint pair here that
12
+ # touches the account rather than the number: {Get} reads review, verification, and
13
+ # ownership state; {Update} renames the account or moves its timezone.
14
+ #
15
+ # Distinct from {Whatsapp::SubscribedApp}, which addresses the same `waba_id` but
16
+ # only toggles webhook delivery, and from {Whatsapp::MessageTemplates}, which
17
+ # manages the templates hanging off that account.
18
+ # See `docs/business_phone_number/account.md`.
19
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api
20
+ module Account
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module Profile
6
+ # The business profile returned by {Get}.
7
+ #
8
+ # Every field arrives only when asked for via `fields`, so every attribute tolerates
9
+ # being absent. Nothing is validated or normalized on the way in — this is read-side
10
+ # data, and a `vertical` Meta adds later must still round-trip untouched. Compare
11
+ # against {Verticals::ALL} rather than expecting this class to have rejected an unknown
12
+ # value, the same reasoning as {Account::Details}' statuses and
13
+ # {Whatsapp::MessageTemplates::Response::Node}'s raw components.
14
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api
15
+ class Details
16
+ # @!attribute [rw] messaging_product
17
+ # @return [String, nil] The messaging service, always `"whatsapp"` in practice.
18
+ attr_accessor :messaging_product
19
+
20
+ # @!attribute [rw] about
21
+ # @return [String, nil] The text shown in the profile's About section.
22
+ attr_accessor :about
23
+
24
+ # @!attribute [rw] address
25
+ # @return [String, nil] The business's physical address.
26
+ attr_accessor :address
27
+
28
+ # @!attribute [rw] description
29
+ # @return [String, nil] The business description.
30
+ attr_accessor :description
31
+
32
+ # @!attribute [rw] email
33
+ # @return [String, nil] The business's contact email address.
34
+ attr_accessor :email
35
+
36
+ # @!attribute [rw] profile_picture_url
37
+ # @return [String, nil] The URL of the profile picture.
38
+ attr_accessor :profile_picture_url
39
+
40
+ # @!attribute [rw] websites
41
+ # @return [Array<String>, nil] The business's website URLs.
42
+ attr_accessor :websites
43
+
44
+ # @!attribute [rw] vertical
45
+ # @return [String, nil] One of {Verticals::ALL}, as sent by Meta.
46
+ attr_accessor :vertical
47
+
48
+ # @param messaging_product [String, nil]
49
+ # @param about [String, nil]
50
+ # @param address [String, nil]
51
+ # @param description [String, nil]
52
+ # @param email [String, nil]
53
+ # @param profile_picture_url [String, nil]
54
+ # @param websites [Array<String>, nil]
55
+ # @param vertical [String, nil]
56
+ def initialize(messaging_product: nil, about: nil, address: nil, description: nil,
57
+ email: nil, profile_picture_url: nil, websites: nil, vertical: nil)
58
+ @messaging_product = messaging_product
59
+ @about = about
60
+ @address = address
61
+ @description = description
62
+ @email = email
63
+ @profile_picture_url = profile_picture_url
64
+ @websites = websites
65
+ @vertical = vertical
66
+ end
67
+
68
+ class << self
69
+ # @param response [Hash, nil] The parsed response body, envelope included.
70
+ # @return [Details]
71
+ def deserialize(response)
72
+ profile = unwrap(response)
73
+
74
+ new(
75
+ messaging_product: profile["messaging_product"],
76
+ about: profile["about"],
77
+ address: profile["address"],
78
+ description: profile["description"],
79
+ email: profile["email"],
80
+ profile_picture_url: profile["profile_picture_url"],
81
+ websites: profile["websites"],
82
+ vertical: profile["vertical"]
83
+ )
84
+ end
85
+
86
+ private
87
+
88
+ # Digs the profile out of Meta's `data` envelope.
89
+ #
90
+ # The reference documents the entry as wrapping the profile in `business_profile`,
91
+ # while live responses are observed to carry the fields directly; both are read
92
+ # rather than betting on one. Only the first entry matters — a phone number has
93
+ # exactly one profile.
94
+ # @param response [Hash, nil]
95
+ # @return [Hash]
96
+ def unwrap(response)
97
+ entry = Array((response || {})["data"]).first || {}
98
+
99
+ entry["business_profile"] || entry
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module Profile
6
+ # Reads a business profile: its about text, description, contact details, websites,
7
+ # industry vertical, and profile picture.
8
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api
9
+ class Get
10
+ extend ResponseHandling
11
+ extend Transport
12
+
13
+ module Defaults
14
+ EDGE = "whatsapp_business_profile"
15
+ end
16
+
17
+ # The fields Meta documents for this edge. Offered for convenience, not
18
+ # enforcement — {.call} deliberately does not validate `fields` against this list
19
+ # (nor do {Account::Get} or {Whatsapp::SubscribedApp::List}), so a field Meta adds
20
+ # later works without a gem release.
21
+ module Fields
22
+ MESSAGING_PRODUCT = "messaging_product"
23
+ ABOUT = "about"
24
+ ADDRESS = "address"
25
+ DESCRIPTION = "description"
26
+ EMAIL = "email"
27
+ PROFILE_PICTURE_URL = "profile_picture_url"
28
+ WEBSITES = "websites"
29
+ VERTICAL = "vertical"
30
+
31
+ ALL = [MESSAGING_PRODUCT, ABOUT, ADDRESS, DESCRIPTION, EMAIL, PROFILE_PICTURE_URL,
32
+ WEBSITES, VERTICAL,].freeze
33
+ end
34
+
35
+ class << self
36
+ # @example Read everything Meta publishes for the profile
37
+ # Get.call(fields: Get::Fields::ALL)
38
+ # @param client [Whatsapp::Client] The WhatsApp client instance.
39
+ # @param fields [Array<String>, String, nil] Restrict the response to these
40
+ # fields; see {Fields::ALL}. Omitted entirely when nil, in which case Meta
41
+ # returns only its own defaults.
42
+ # @return [Details]
43
+ # @raise [Error] if no phone number ID is configured, or the request fails.
44
+ def call(client: Client.new, fields: nil)
45
+ params = fields ? { fields: Array(fields).join(",") } : {}
46
+ response = client.connection.get(edge_path(client, Defaults::EDGE), params:)
47
+
48
+ Details.deserialize(
49
+ parse_json(handle_response!(response, error_class: Error, action: "get business profile details"))
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end