ruby-whatsapp 0.4.0 → 0.6.1

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/README.md +322 -617
  4. data/docs/README.md +76 -0
  5. data/docs/business_phone_number/README.md +243 -0
  6. data/docs/business_phone_number/account.md +170 -0
  7. data/docs/business_phone_number/contact_book.md +90 -0
  8. data/docs/business_phone_number/profile.md +220 -0
  9. data/docs/business_phone_number/username.md +157 -0
  10. data/docs/configuration.md +177 -0
  11. data/docs/errors.md +219 -0
  12. data/docs/media/README.md +161 -0
  13. data/docs/message_templates/README.md +206 -0
  14. data/docs/message_templates/authentication.md +107 -0
  15. data/docs/message_templates/carousel.md +103 -0
  16. data/docs/message_templates/components.md +215 -0
  17. data/docs/message_templates/library.md +98 -0
  18. data/docs/message_templates/limited_time_offer.md +94 -0
  19. data/docs/message_templates/responses.md +173 -0
  20. data/docs/message_templates/standard.md +138 -0
  21. data/docs/messages/README.md +212 -0
  22. data/docs/messages/address.md +86 -0
  23. data/docs/messages/audio.md +43 -0
  24. data/docs/messages/contacts.md +129 -0
  25. data/docs/messages/document.md +49 -0
  26. data/docs/messages/image.md +59 -0
  27. data/docs/messages/interactive.md +342 -0
  28. data/docs/messages/location.md +51 -0
  29. data/docs/messages/location_request.md +59 -0
  30. data/docs/messages/mark_message_as_read.md +84 -0
  31. data/docs/messages/reaction.md +59 -0
  32. data/docs/messages/sticker.md +40 -0
  33. data/docs/messages/template.md +193 -0
  34. data/docs/messages/text.md +55 -0
  35. data/docs/messages/video.md +41 -0
  36. data/docs/schedule/README.md +183 -0
  37. data/docs/subscribed_app/README.md +139 -0
  38. data/docs/webhooks/README.md +217 -0
  39. data/docs/webhooks/account_alerts.md +44 -0
  40. data/docs/webhooks/account_review_update.md +29 -0
  41. data/docs/webhooks/account_update.md +54 -0
  42. data/docs/webhooks/automatic_events.md +47 -0
  43. data/docs/webhooks/business_capability_update.md +40 -0
  44. data/docs/webhooks/business_username_updates.md +55 -0
  45. data/docs/webhooks/history.md +107 -0
  46. data/docs/webhooks/message_template_components_update.md +46 -0
  47. data/docs/webhooks/message_template_quality_update.md +49 -0
  48. data/docs/webhooks/message_template_status_update.md +55 -0
  49. data/docs/webhooks/messages.md +520 -0
  50. data/docs/webhooks/partner_solutions.md +35 -0
  51. data/docs/webhooks/payment_configuration_update.md +40 -0
  52. data/docs/webhooks/phone_number_name_update.md +44 -0
  53. data/docs/webhooks/phone_number_quality_update.md +40 -0
  54. data/docs/webhooks/security.md +43 -0
  55. data/docs/webhooks/smb_app_state_sync.md +91 -0
  56. data/docs/webhooks/smb_message_echoes.md +91 -0
  57. data/docs/webhooks/template_category_update.md +52 -0
  58. data/docs/webhooks/user_id_update.md +84 -0
  59. data/docs/webhooks/user_preferences.md +87 -0
  60. data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
  61. data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
  62. data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
  63. data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
  64. data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
  65. data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
  66. data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
  67. data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
  68. data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
  69. data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
  70. data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
  71. data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
  72. data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
  73. data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
  74. data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
  75. data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
  76. data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
  77. data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
  78. data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
  79. data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
  80. data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
  81. data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
  82. data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
  83. data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
  84. data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
  85. data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
  86. data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
  87. data/lib/ruby/whatsapp/message_templates.rb +2 -6
  88. data/lib/ruby/whatsapp/messages/base.rb +47 -5
  89. data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
  90. data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
  91. data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
  92. data/lib/ruby/whatsapp/path_building.rb +33 -0
  93. data/lib/ruby/whatsapp/schedule/create.rb +101 -0
  94. data/lib/ruby/whatsapp/schedule/list.rb +50 -0
  95. data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
  96. data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
  97. data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
  98. data/lib/ruby/whatsapp/schedule/response.rb +18 -0
  99. data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
  100. data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
  101. data/lib/ruby/whatsapp/schedule.rb +15 -0
  102. data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
  103. data/lib/ruby/whatsapp/version.rb +1 -1
  104. data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
  105. data/lib/ruby/whatsapp/webhook/change.rb +3 -1
  106. data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
  107. data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
  108. data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
  109. data/lib/ruby/whatsapp/webhook/history.rb +73 -34
  110. data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
  111. data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
  112. data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
  113. data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
  114. data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
  115. data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
  116. data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
  117. data/lib/ruby/whatsapp/webhook/message.rb +3 -0
  118. data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
  119. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
  120. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
  121. data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
  122. data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
  123. data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
  124. data/lib/ruby/whatsapp/webhook/status.rb +23 -2
  125. data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
  126. data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
  127. data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
  128. data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
  129. data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
  130. metadata +100 -1
@@ -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,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module ContactBook
6
+ # Deletes one user's entry from your contact book.
7
+ #
8
+ # A validated instance rather than a bare class method: Meta documents a rule this
9
+ # can check — parent BSUIDs are not accepted here — and the edge is rate limited, so
10
+ # a request that was never going to be accepted should not be spent.
11
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
12
+ class Delete
13
+ include ActiveModel::Validations
14
+ extend ResponseHandling
15
+ extend Transport
16
+
17
+ module Defaults
18
+ EDGE = "contact_book"
19
+ MESSAGING_PRODUCT = "whatsapp"
20
+ # A parent BSUID carries an extra segment (`US.ENT.118...`) where a standard one
21
+ # has just the country code and identifier (`US.134...`), so counting separators
22
+ # distinguishes them without asserting a country-code grammar Meta has not
23
+ # published. See {#validate_standard_bsuid}.
24
+ PARENT_SEPARATOR_COUNT = 1
25
+ end
26
+
27
+ # @!attribute [rw] bsuid
28
+ # @return [String] The business-scoped user ID whose entry to remove.
29
+ attr_accessor :bsuid
30
+
31
+ validate :validate_standard_bsuid
32
+ validates :bsuid, presence: true
33
+
34
+ # @param bsuid [String] The user's standard BSUID, e.g.
35
+ # `"US.13491208655302741918"`. Must belong to the same business portfolio as the
36
+ # phone number — Meta checks that server-side, since it is account state this gem
37
+ # cannot see. Parent BSUIDs are not supported and are rejected locally.
38
+ # @raise [ActiveModel::ValidationError] if the BSUID is missing or looks like a parent.
39
+ def initialize(bsuid:)
40
+ @bsuid = bsuid
41
+
42
+ validate!
43
+ end
44
+
45
+ # @return [Hash] The query parameters. Sent as a query string rather than a body
46
+ # because the edge is a `DELETE` — the one action in this directory whose
47
+ # `#serialize` is not a JSON payload. `messaging_product` is documented required,
48
+ # so it is always sent.
49
+ def serialize
50
+ { messaging_product: Defaults::MESSAGING_PRODUCT, bsuid: }
51
+ end
52
+
53
+ class << self
54
+ # @example Forget a user who asked to be forgotten
55
+ # ContactBook::Delete.call(bsuid: "US.13491208655302741918")
56
+ # @param client [Whatsapp::Client] The WhatsApp client instance.
57
+ # @param bsuid [String] See {#initialize}.
58
+ # @return [Deletion] Whose `deleted` is `false` if no entry existed.
59
+ # @raise [ActiveModel::ValidationError] if the BSUID is missing or looks like a parent.
60
+ # @raise [Error] if no phone number ID is configured, or the request fails.
61
+ def call(bsuid:, client: Client.new)
62
+ params = new(bsuid:).serialize
63
+ response = client.connection.delete(edge_path(client, Defaults::EDGE), params:)
64
+
65
+ Deletion.deserialize(
66
+ parse_json(handle_response!(response, error_class: Error, action: "delete contact book entry"))
67
+ )
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Meta states plainly that parent BSUIDs are rejected here, and this release hands
74
+ # callers `user_id` and `parent_user_id` side by side on half a dozen webhook
75
+ # classes, so mixing them up is the predictable mistake. Detected by segment count
76
+ # rather than by matching `ENT`, which would bet that `ENT` is the only marker Meta
77
+ # will ever insert — an inference either way, so the weaker one wins.
78
+ # @return [void]
79
+ def validate_standard_bsuid
80
+ return if bsuid.nil? || bsuid.to_s.count(".") <= Defaults::PARENT_SEPARATOR_COUNT
81
+
82
+ errors.add(:bsuid, "looks like a parent BSUID; the contact book accepts standard BSUIDs only")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ module ContactBook
6
+ # The result of a {Delete}: whether the request worked, and whether it found anything.
7
+ #
8
+ # Its own class rather than {BusinessPhoneNumber::Response}, which carries only
9
+ # `success` (plus the `id` {VerifyCode} sets). `deleted` is a second, independent
10
+ # boolean that changes how `success` reads — `success: true, deleted: false` means
11
+ # the request was fine and there was simply no entry — and collapsing the two would
12
+ # lose that. Adding it to the class every onboarding action returns would also put a
13
+ # field on five actions that can never set it.
14
+ class Deletion
15
+ # @!attribute [rw] messaging_product
16
+ # @return [String, nil] Always `"whatsapp"`.
17
+ attr_accessor :messaging_product
18
+
19
+ # @!attribute [rw] success
20
+ # @return [Boolean] Whether the request was processed.
21
+ attr_accessor :success
22
+
23
+ # @!attribute [rw] deleted
24
+ # @return [Boolean] Whether an entry existed and was removed. `false` alongside a
25
+ # `true` {#success} means there was nothing stored for that BSUID — not a failure.
26
+ attr_accessor :deleted
27
+
28
+ def initialize(messaging_product: nil, success: false, deleted: false)
29
+ @messaging_product = messaging_product
30
+ @success = success
31
+ @deleted = deleted
32
+ end
33
+
34
+ class << self
35
+ # @param response [Hash, nil] The parsed response body.
36
+ # @return [Deletion]
37
+ def deserialize(response)
38
+ response ||= {}
39
+
40
+ new(
41
+ messaging_product: response["messaging_product"],
42
+ success: response["success"] == true,
43
+ deleted: response["deleted"] == true
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whatsapp
4
+ module BusinessPhoneNumber
5
+ # Removes a stored user contact from your **contact book** — the store Meta keeps of
6
+ # the phone numbers and BSUIDs you have interacted with, and uses to keep putting a
7
+ # user's phone number in your webhooks after they adopt a username.
8
+ #
9
+ # Addressed as a `phone_id` edge (`DELETE /{phone_id}/contact_book`), so it reuses
10
+ # {BusinessPhoneNumber::Transport} unchanged, like {Profile} and {Username}. It sits
11
+ # here rather than in a top-level module for two reasons: this directory is the
12
+ # administration of one phone number, as opposed to {Whatsapp::Media} and
13
+ # {Whatsapp::Messages}, which are the traffic itself; and a top-level module would
14
+ # need its own error class for a single endpoint, where living here means one
15
+ # `rescue BusinessPhoneNumber::Error` still covers the whole directory.
16
+ #
17
+ # Note the asymmetry between how it is addressed and what it affects: the edge is on
18
+ # one phone number, but the contact book is scoped to the whole **business portfolio**,
19
+ # so a deletion removes that user's phone number from webhooks for *every* number in
20
+ # the portfolio. See `docs/business_phone_number/contact_book.md`.
21
+ # Source: https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
22
+ module ContactBook
23
+ end
24
+ end
25
+ 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