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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +276 -634
- data/docs/README.md +73 -0
- data/docs/business_phone_number/README.md +232 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/configuration.md +172 -0
- data/docs/errors.md +208 -0
- data/docs/media/README.md +161 -0
- data/docs/message_templates/README.md +206 -0
- data/docs/message_templates/authentication.md +107 -0
- data/docs/message_templates/carousel.md +103 -0
- data/docs/message_templates/components.md +179 -0
- data/docs/message_templates/library.md +98 -0
- data/docs/message_templates/limited_time_offer.md +94 -0
- data/docs/message_templates/responses.md +173 -0
- data/docs/message_templates/standard.md +138 -0
- data/docs/messages/README.md +135 -0
- data/docs/messages/address.md +86 -0
- data/docs/messages/audio.md +43 -0
- data/docs/messages/contacts.md +129 -0
- data/docs/messages/document.md +49 -0
- data/docs/messages/image.md +59 -0
- data/docs/messages/interactive.md +281 -0
- data/docs/messages/location.md +51 -0
- data/docs/messages/location_request.md +59 -0
- data/docs/messages/mark_message_as_read.md +84 -0
- data/docs/messages/reaction.md +59 -0
- data/docs/messages/sticker.md +40 -0
- data/docs/messages/template.md +166 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +209 -0
- data/docs/webhooks/account_alerts.md +44 -0
- data/docs/webhooks/account_review_update.md +29 -0
- data/docs/webhooks/account_update.md +54 -0
- data/docs/webhooks/automatic_events.md +47 -0
- data/docs/webhooks/business_capability_update.md +40 -0
- data/docs/webhooks/history.md +44 -0
- data/docs/webhooks/message_template_components_update.md +46 -0
- data/docs/webhooks/message_template_quality_update.md +49 -0
- data/docs/webhooks/message_template_status_update.md +55 -0
- data/docs/webhooks/messages.md +385 -0
- data/docs/webhooks/partner_solutions.md +35 -0
- data/docs/webhooks/payment_configuration_update.md +40 -0
- data/docs/webhooks/phone_number_name_update.md +44 -0
- data/docs/webhooks/phone_number_quality_update.md +40 -0
- data/docs/webhooks/security.md +43 -0
- data/docs/webhooks/smb_app_state_sync.md +53 -0
- data/docs/webhooks/smb_message_echoes.md +54 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_preferences.md +48 -0
- data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
- data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
- data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
- data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
- data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
- data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
- data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +11 -12
- data/lib/ruby/whatsapp/business_phone_number.rb +11 -4
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- 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
|