flow_chat 0.6.1 → 0.7.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +84 -1229
- data/docs/configuration.md +337 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +306 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +9 -18
- data/examples/ussd_controller.rb +32 -38
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +48 -2
- data/lib/flow_chat/config.rb +5 -0
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +37 -4
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +6 -6
- data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +14 -0
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +120 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +8 -0
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +22 -11
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
data/docs/ussd-setup.md
ADDED
@@ -0,0 +1,306 @@
|
|
1
|
+
# USSD Setup Guide
|
2
|
+
|
3
|
+
This guide covers USSD configuration and implementation patterns for FlowChat.
|
4
|
+
|
5
|
+
## Basic Setup
|
6
|
+
|
7
|
+
### 1. Create a Flow
|
8
|
+
|
9
|
+
Create a flow in `app/flow_chat/welcome_flow.rb`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class WelcomeFlow < FlowChat::Flow
|
13
|
+
def main_page
|
14
|
+
name = app.screen(:name) do |prompt|
|
15
|
+
prompt.ask "Welcome! What's your name?",
|
16
|
+
transform: ->(input) { input.strip.titleize }
|
17
|
+
end
|
18
|
+
|
19
|
+
service = app.screen(:service) do |prompt|
|
20
|
+
prompt.select "Choose a service:", {
|
21
|
+
"balance" => "Check Balance",
|
22
|
+
"transfer" => "Transfer Money",
|
23
|
+
"history" => "Transaction History"
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
case service
|
28
|
+
when "balance"
|
29
|
+
show_balance
|
30
|
+
when "transfer"
|
31
|
+
transfer_money
|
32
|
+
when "history"
|
33
|
+
show_history
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def show_balance
|
40
|
+
# Simulate balance check
|
41
|
+
balance = fetch_user_balance(app.phone_number)
|
42
|
+
app.say "Hello #{app.session.get(:name)}! Your balance is $#{balance}."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
### 2. Create Controller
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class UssdController < ApplicationController
|
51
|
+
skip_forgery_protection
|
52
|
+
|
53
|
+
def process_request
|
54
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
55
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
56
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
57
|
+
end
|
58
|
+
|
59
|
+
processor.run WelcomeFlow, :main_page
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### 3. Configure Routes
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# config/routes.rb
|
68
|
+
Rails.application.routes.draw do
|
69
|
+
post 'ussd' => 'ussd#process_request'
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
## Gateway Configuration
|
74
|
+
|
75
|
+
### Nalo Gateway
|
76
|
+
|
77
|
+
FlowChat currently supports Nalo USSD gateways:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
81
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
82
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
The Nalo gateway expects these parameters in the request:
|
87
|
+
- `msisdn` - User's phone number
|
88
|
+
- `input` - User's input text
|
89
|
+
- `session_id` - Session identifier
|
90
|
+
|
91
|
+
## Pagination
|
92
|
+
|
93
|
+
USSD messages are automatically paginated when they exceed character limits:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# Configure pagination behavior
|
97
|
+
FlowChat::Config.ussd.pagination_page_size = 140 # characters per page
|
98
|
+
FlowChat::Config.ussd.pagination_next_option = "#" # option for next page
|
99
|
+
FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
|
100
|
+
FlowChat::Config.ussd.pagination_back_option = "0" # option for previous page
|
101
|
+
FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
|
102
|
+
```
|
103
|
+
|
104
|
+
### Example Long Message
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
def show_terms
|
108
|
+
terms = "Very long terms and conditions text that exceeds 140 characters..."
|
109
|
+
app.say terms # Automatically paginated
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
**USSD Output:**
|
114
|
+
```
|
115
|
+
Terms and Conditions:
|
116
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt...
|
117
|
+
|
118
|
+
# More
|
119
|
+
0 Back
|
120
|
+
```
|
121
|
+
|
122
|
+
## Resumable Sessions
|
123
|
+
|
124
|
+
Enable resumable sessions for better user experience:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
128
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
129
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
130
|
+
config.use_resumable_sessions # Enable resumable sessions
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
Users can continue interrupted conversations within the timeout period.
|
135
|
+
|
136
|
+
## Middleware
|
137
|
+
|
138
|
+
### Custom Middleware
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class AuthenticationMiddleware
|
142
|
+
def initialize(app)
|
143
|
+
@app = app
|
144
|
+
end
|
145
|
+
|
146
|
+
def call(context)
|
147
|
+
phone = context["request.msisdn"]
|
148
|
+
|
149
|
+
unless user_exists?(phone)
|
150
|
+
# Return appropriate response for unregistered user
|
151
|
+
return [:prompt, "Please register first. Visit our website to sign up.", nil, nil]
|
152
|
+
end
|
153
|
+
|
154
|
+
@app.call(context)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def user_exists?(phone)
|
160
|
+
User.exists?(phone: phone)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Use custom middleware
|
165
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
166
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
167
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
168
|
+
config.use_middleware AuthenticationMiddleware
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
## Advanced Patterns
|
173
|
+
|
174
|
+
### Menu-Driven Navigation
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
class MenuFlow < FlowChat::Flow
|
178
|
+
def main_page
|
179
|
+
choice = app.screen(:main_menu) do |prompt|
|
180
|
+
prompt.select "Main Menu:", {
|
181
|
+
"1" => "Account Services",
|
182
|
+
"2" => "Transfer Services",
|
183
|
+
"3" => "Customer Support"
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
case choice
|
188
|
+
when "1"
|
189
|
+
account_services
|
190
|
+
when "2"
|
191
|
+
transfer_services
|
192
|
+
when "3"
|
193
|
+
customer_support
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def account_services
|
198
|
+
choice = app.screen(:account_menu) do |prompt|
|
199
|
+
prompt.select "Account Services:", {
|
200
|
+
"1" => "Check Balance",
|
201
|
+
"2" => "Mini Statement",
|
202
|
+
"0" => "Back to Main Menu"
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
case choice
|
207
|
+
when "1"
|
208
|
+
check_balance
|
209
|
+
when "2"
|
210
|
+
mini_statement
|
211
|
+
when "0"
|
212
|
+
main_page # Return to main menu
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
### Error Handling
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
def transfer_money
|
222
|
+
begin
|
223
|
+
amount = app.screen(:amount) do |prompt|
|
224
|
+
prompt.ask "Enter amount:",
|
225
|
+
validate: ->(input) {
|
226
|
+
return "Amount must be numeric" unless input.match?(/^\d+(\.\d{2})?$/)
|
227
|
+
return "Minimum transfer is $1" unless input.to_f >= 1.0
|
228
|
+
nil
|
229
|
+
},
|
230
|
+
transform: ->(input) { input.to_f }
|
231
|
+
end
|
232
|
+
|
233
|
+
recipient = app.screen(:recipient) do |prompt|
|
234
|
+
prompt.ask "Enter recipient phone number:",
|
235
|
+
validate: ->(input) {
|
236
|
+
return "Invalid phone number" unless valid_phone?(input)
|
237
|
+
nil
|
238
|
+
}
|
239
|
+
end
|
240
|
+
|
241
|
+
process_transfer(amount, recipient)
|
242
|
+
app.say "Transfer of $#{amount} to #{recipient} successful!"
|
243
|
+
|
244
|
+
rescue TransferError => e
|
245
|
+
app.say "Transfer failed: #{e.message}. Please try again."
|
246
|
+
transfer_money # Retry
|
247
|
+
end
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
### Session Data Management
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
class RegistrationFlow < FlowChat::Flow
|
255
|
+
def main_page
|
256
|
+
collect_personal_info
|
257
|
+
collect_preferences
|
258
|
+
confirm_and_save
|
259
|
+
end
|
260
|
+
|
261
|
+
private
|
262
|
+
|
263
|
+
def collect_personal_info
|
264
|
+
app.screen(:first_name) { |p| p.ask "First name:" }
|
265
|
+
app.screen(:last_name) { |p| p.ask "Last name:" }
|
266
|
+
app.screen(:email) { |p| p.ask "Email address:" }
|
267
|
+
end
|
268
|
+
|
269
|
+
def collect_preferences
|
270
|
+
app.screen(:language) { |p| p.select "Language:", ["English", "French"] }
|
271
|
+
app.screen(:notifications) { |p| p.yes? "Enable SMS notifications?" }
|
272
|
+
end
|
273
|
+
|
274
|
+
def confirm_and_save
|
275
|
+
summary = build_summary_from_session
|
276
|
+
confirmed = app.screen(:confirm) { |p| p.yes? "Save profile?\n\n#{summary}" }
|
277
|
+
|
278
|
+
if confirmed
|
279
|
+
save_user_profile
|
280
|
+
app.say "Registration complete!"
|
281
|
+
else
|
282
|
+
app.say "Registration cancelled."
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def build_summary_from_session
|
287
|
+
first_name = app.session.get(:first_name)
|
288
|
+
last_name = app.session.get(:last_name)
|
289
|
+
email = app.session.get(:email)
|
290
|
+
language = app.session.get(:language)
|
291
|
+
|
292
|
+
"Name: #{first_name} #{last_name}\nEmail: #{email}\nLanguage: #{language}"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
## Testing USSD Flows
|
298
|
+
|
299
|
+
Use the built-in simulator for testing USSD flows:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
# config/initializers/flowchat.rb
|
303
|
+
FlowChat::Config.simulator_secret = "your_secure_secret"
|
304
|
+
```
|
305
|
+
|
306
|
+
See [Testing Guide](testing.md) for comprehensive testing strategies and [examples/ussd_controller.rb](../examples/ussd_controller.rb) for complete implementation examples.
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# WhatsApp Setup Guide
|
2
|
+
|
3
|
+
This guide covers comprehensive WhatsApp configuration for FlowChat, including security, multi-tenant setups, and different processing modes.
|
4
|
+
|
5
|
+
## Credential Configuration
|
6
|
+
|
7
|
+
FlowChat supports multiple ways to configure WhatsApp credentials:
|
8
|
+
|
9
|
+
### Option 1: Rails Credentials
|
10
|
+
|
11
|
+
```bash
|
12
|
+
rails credentials:edit
|
13
|
+
```
|
14
|
+
|
15
|
+
```yaml
|
16
|
+
whatsapp:
|
17
|
+
access_token: "your_access_token"
|
18
|
+
phone_number_id: "your_phone_number_id"
|
19
|
+
verify_token: "your_verify_token"
|
20
|
+
app_id: "your_app_id"
|
21
|
+
app_secret: "your_app_secret"
|
22
|
+
business_account_id: "your_business_account_id"
|
23
|
+
skip_signature_validation: false # Set to true only for development/testing
|
24
|
+
```
|
25
|
+
|
26
|
+
### Option 2: Environment Variables
|
27
|
+
|
28
|
+
```bash
|
29
|
+
export WHATSAPP_ACCESS_TOKEN="your_access_token"
|
30
|
+
export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
|
31
|
+
export WHATSAPP_VERIFY_TOKEN="your_verify_token"
|
32
|
+
export WHATSAPP_APP_ID="your_app_id"
|
33
|
+
export WHATSAPP_APP_SECRET="your_app_secret"
|
34
|
+
export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
|
35
|
+
export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
|
36
|
+
```
|
37
|
+
|
38
|
+
### Option 3: Per-Setup Configuration
|
39
|
+
|
40
|
+
For multi-tenant applications:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
custom_config = FlowChat::Whatsapp::Configuration.new
|
44
|
+
custom_config.access_token = "your_specific_access_token"
|
45
|
+
custom_config.phone_number_id = "your_specific_phone_number_id"
|
46
|
+
custom_config.verify_token = "your_specific_verify_token"
|
47
|
+
custom_config.app_id = "your_specific_app_id"
|
48
|
+
custom_config.app_secret = "your_specific_app_secret"
|
49
|
+
custom_config.business_account_id = "your_specific_business_account_id"
|
50
|
+
custom_config.skip_signature_validation = false
|
51
|
+
|
52
|
+
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
53
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_config
|
54
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
## Message Handling Modes
|
59
|
+
|
60
|
+
Configure message processing behavior in `config/initializers/flowchat.rb`:
|
61
|
+
|
62
|
+
### Inline Mode (Default)
|
63
|
+
|
64
|
+
Process messages synchronously:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
68
|
+
```
|
69
|
+
|
70
|
+
### Background Mode
|
71
|
+
|
72
|
+
Process flows synchronously, send responses asynchronously:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
FlowChat::Config.whatsapp.message_handling_mode = :background
|
76
|
+
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
77
|
+
```
|
78
|
+
|
79
|
+
### Simulator Mode
|
80
|
+
|
81
|
+
Return response data instead of sending via WhatsApp API:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
85
|
+
```
|
86
|
+
|
87
|
+
## Security Configuration
|
88
|
+
|
89
|
+
### Webhook Signature Validation (Production)
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
custom_config.app_secret = "your_whatsapp_app_secret"
|
93
|
+
custom_config.skip_signature_validation = false # Default: enforce validation
|
94
|
+
```
|
95
|
+
|
96
|
+
### Development Mode
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
custom_config.app_secret = nil
|
100
|
+
custom_config.skip_signature_validation = true # Disable validation
|
101
|
+
```
|
102
|
+
|
103
|
+
⚠️ **Security Warning**: Only disable signature validation in development/testing environments.
|
104
|
+
|
105
|
+
## Multi-Tenant Setup
|
106
|
+
|
107
|
+
See [examples/multi_tenant_whatsapp_controller.rb](../examples/multi_tenant_whatsapp_controller.rb) for a complete multi-tenant implementation.
|
108
|
+
|
109
|
+
## Background Job Setup
|
110
|
+
|
111
|
+
When using background jobs with programmatic WhatsApp configurations, you must register configurations in an initializer so they're available to background jobs:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# config/initializers/whatsapp_configs.rb
|
115
|
+
tenant_a_config = FlowChat::Whatsapp::Configuration.new(:tenant_a)
|
116
|
+
tenant_a_config.access_token = "tenant_a_token"
|
117
|
+
tenant_a_config.phone_number_id = "tenant_a_phone"
|
118
|
+
tenant_a_config.verify_token = "tenant_a_verify"
|
119
|
+
tenant_a_config.app_secret = "tenant_a_secret"
|
120
|
+
# Configuration is automatically registered as :tenant_a
|
121
|
+
|
122
|
+
tenant_b_config = FlowChat::Whatsapp::Configuration.new(:tenant_b)
|
123
|
+
tenant_b_config.access_token = "tenant_b_token"
|
124
|
+
tenant_b_config.phone_number_id = "tenant_b_phone"
|
125
|
+
tenant_b_config.verify_token = "tenant_b_verify"
|
126
|
+
tenant_b_config.app_secret = "tenant_b_secret"
|
127
|
+
# Configuration is automatically registered as :tenant_b
|
128
|
+
```
|
129
|
+
|
130
|
+
Then reference the named configuration in your controller:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
134
|
+
# Use registered configuration by name
|
135
|
+
tenant_config = FlowChat::Whatsapp::Configuration.get(:tenant_a)
|
136
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, tenant_config
|
137
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
**Why this is required:** Background jobs run in a separate context and cannot access configurations created in controller actions. Registered configurations are globally available.
|
142
|
+
|
143
|
+
**Alternative:** Override the job's configuration resolution logic (see examples).
|
144
|
+
|
145
|
+
See [examples/whatsapp_message_job.rb](../examples/whatsapp_message_job.rb) for job implementation and [Background Jobs Guide](background-jobs.md) for detailed setup.
|
146
|
+
|
147
|
+
## Troubleshooting
|
148
|
+
|
149
|
+
### Common Issues
|
150
|
+
|
151
|
+
**Configuration Error**: Check that all required credentials are set
|
152
|
+
**Signature Validation Failed**: Verify app_secret matches your WhatsApp app
|
153
|
+
**Timeout Issues**: Consider using background mode for high-volume applications
|
154
|
+
|
155
|
+
### Debug Mode
|
156
|
+
|
157
|
+
Enable debug logging in development:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
# config/environments/development.rb
|
161
|
+
config.log_level = :debug
|
162
|
+
```
|
@@ -1,26 +1,19 @@
|
|
1
1
|
# Example Multi-Tenant WhatsApp Controller
|
2
2
|
# This shows how to configure different WhatsApp accounts per tenant/client
|
3
3
|
|
4
|
+
# Controller supporting multiple WhatsApp accounts per tenant
|
4
5
|
class MultiTenantWhatsappController < ApplicationController
|
5
6
|
skip_forgery_protection
|
6
7
|
|
7
8
|
def webhook
|
8
|
-
# Determine tenant from subdomain, path, or other logic
|
9
9
|
tenant = determine_tenant(request)
|
10
|
-
|
11
|
-
# Get tenant-specific WhatsApp configuration
|
12
10
|
whatsapp_config = get_whatsapp_config_for_tenant(tenant)
|
13
11
|
|
14
|
-
|
15
|
-
# This allows testing different tenant endpoints via the simulator interface
|
16
|
-
enable_simulator = Rails.env.development? || Rails.env.staging?
|
17
|
-
|
18
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: enable_simulator) do |config|
|
12
|
+
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
|
19
13
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
|
20
14
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
21
15
|
end
|
22
16
|
|
23
|
-
# Use tenant-specific flow
|
24
17
|
flow_class = get_flow_for_tenant(tenant)
|
25
18
|
processor.run flow_class, :main_page
|
26
19
|
end
|
@@ -31,14 +24,13 @@ class MultiTenantWhatsappController < ApplicationController
|
|
31
24
|
# Option 1: From subdomain
|
32
25
|
return request.subdomain if request.subdomain.present?
|
33
26
|
|
34
|
-
# Option 2: From path
|
27
|
+
# Option 2: From path (e.g., /whatsapp/acme/webhook)
|
35
28
|
tenant_from_path = request.path.match(%r{^/whatsapp/(\w+)/})&.captures&.first
|
36
29
|
return tenant_from_path if tenant_from_path
|
37
30
|
|
38
|
-
# Option 3: From
|
31
|
+
# Option 3: From header
|
39
32
|
return request.headers["X-Tenant-ID"] if request.headers["X-Tenant-ID"]
|
40
33
|
|
41
|
-
# Fallback to default
|
42
34
|
"default"
|
43
35
|
end
|
44
36
|
|
@@ -49,9 +41,7 @@ class MultiTenantWhatsappController < ApplicationController
|
|
49
41
|
config.access_token = ENV["ACME_WHATSAPP_ACCESS_TOKEN"]
|
50
42
|
config.phone_number_id = ENV["ACME_WHATSAPP_PHONE_NUMBER_ID"]
|
51
43
|
config.verify_token = ENV["ACME_WHATSAPP_VERIFY_TOKEN"]
|
52
|
-
config.app_id = ENV["ACME_WHATSAPP_APP_ID"]
|
53
44
|
config.app_secret = ENV["ACME_WHATSAPP_APP_SECRET"]
|
54
|
-
config.business_account_id = ENV["ACME_WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
55
45
|
end
|
56
46
|
|
57
47
|
when "tech_startup"
|
@@ -59,9 +49,7 @@ class MultiTenantWhatsappController < ApplicationController
|
|
59
49
|
config.access_token = ENV["TECHSTARTUP_WHATSAPP_ACCESS_TOKEN"]
|
60
50
|
config.phone_number_id = ENV["TECHSTARTUP_WHATSAPP_PHONE_NUMBER_ID"]
|
61
51
|
config.verify_token = ENV["TECHSTARTUP_WHATSAPP_VERIFY_TOKEN"]
|
62
|
-
config.app_id = ENV["TECHSTARTUP_WHATSAPP_APP_ID"]
|
63
52
|
config.app_secret = ENV["TECHSTARTUP_WHATSAPP_APP_SECRET"]
|
64
|
-
config.business_account_id = ENV["TECHSTARTUP_WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
65
53
|
end
|
66
54
|
|
67
55
|
when "retail_store"
|
@@ -71,13 +59,10 @@ class MultiTenantWhatsappController < ApplicationController
|
|
71
59
|
config.access_token = tenant_config.access_token
|
72
60
|
config.phone_number_id = tenant_config.phone_number_id
|
73
61
|
config.verify_token = tenant_config.verify_token
|
74
|
-
config.app_id = tenant_config.app_id
|
75
62
|
config.app_secret = tenant_config.app_secret
|
76
|
-
config.business_account_id = tenant_config.business_account_id
|
77
63
|
end
|
78
64
|
|
79
65
|
else
|
80
|
-
# Use default/global configuration
|
81
66
|
FlowChat::Whatsapp::Configuration.from_credentials
|
82
67
|
end
|
83
68
|
end
|
@@ -91,7 +76,7 @@ class MultiTenantWhatsappController < ApplicationController
|
|
91
76
|
when "retail_store"
|
92
77
|
RetailStoreFlow
|
93
78
|
else
|
94
|
-
WelcomeFlow
|
79
|
+
WelcomeFlow
|
95
80
|
end
|
96
81
|
end
|
97
82
|
end
|
@@ -101,21 +86,14 @@ class DatabaseWhatsappController < ApplicationController
|
|
101
86
|
skip_forgery_protection
|
102
87
|
|
103
88
|
def webhook
|
104
|
-
# Get account from business phone number or other identifier
|
105
89
|
business_account = find_business_account(params)
|
90
|
+
return head :not_found if business_account.nil?
|
106
91
|
|
107
|
-
if business_account.nil?
|
108
|
-
return head :not_found
|
109
|
-
end
|
110
|
-
|
111
|
-
# Create configuration from database record
|
112
92
|
whatsapp_config = FlowChat::Whatsapp::Configuration.new.tap do |config|
|
113
93
|
config.access_token = business_account.whatsapp_access_token
|
114
94
|
config.phone_number_id = business_account.whatsapp_phone_number_id
|
115
95
|
config.verify_token = business_account.whatsapp_verify_token
|
116
|
-
config.app_id = business_account.whatsapp_app_id
|
117
96
|
config.app_secret = business_account.whatsapp_app_secret
|
118
|
-
config.business_account_id = business_account.whatsapp_business_account_id
|
119
97
|
end
|
120
98
|
|
121
99
|
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
@@ -129,20 +107,14 @@ class DatabaseWhatsappController < ApplicationController
|
|
129
107
|
private
|
130
108
|
|
131
109
|
def find_business_account(params)
|
132
|
-
#
|
133
|
-
# 1. Phone number ID from webhook
|
134
|
-
# 2. Business account ID from webhook
|
135
|
-
# 3. Custom routing parameter
|
136
|
-
|
137
|
-
# Example: Find by phone number ID in webhook
|
110
|
+
# Find by phone number ID from webhook
|
138
111
|
phone_number_id = extract_phone_number_id_from_webhook(params)
|
139
112
|
BusinessAccount.find_by(whatsapp_phone_number_id: phone_number_id)
|
140
113
|
end
|
141
114
|
|
142
115
|
def extract_phone_number_id_from_webhook(params)
|
143
|
-
# Extract from webhook payload structure
|
144
|
-
|
145
|
-
nil
|
116
|
+
# Extract from webhook payload - implement based on your structure
|
117
|
+
params.dig(:entry, 0, :changes, 0, :value, :metadata, :phone_number_id)
|
146
118
|
end
|
147
119
|
end
|
148
120
|
|
@@ -10,12 +10,12 @@ class SimulatorController < ApplicationController
|
|
10
10
|
|
11
11
|
protected
|
12
12
|
|
13
|
-
# Define different
|
13
|
+
# Define different endpoints to test
|
14
14
|
def configurations
|
15
15
|
{
|
16
16
|
ussd_main: {
|
17
17
|
name: "Main USSD Endpoint",
|
18
|
-
description: "Primary USSD integration
|
18
|
+
description: "Primary USSD integration",
|
19
19
|
processor_type: "ussd",
|
20
20
|
provider: "nalo",
|
21
21
|
endpoint: "/ussd",
|
@@ -23,26 +23,17 @@ class SimulatorController < ApplicationController
|
|
23
23
|
color: "#28a745"
|
24
24
|
},
|
25
25
|
whatsapp_main: {
|
26
|
-
name: "Main WhatsApp Endpoint",
|
27
|
-
description: "Primary WhatsApp webhook
|
26
|
+
name: "Main WhatsApp Endpoint",
|
27
|
+
description: "Primary WhatsApp webhook",
|
28
28
|
processor_type: "whatsapp",
|
29
29
|
provider: "cloud_api",
|
30
30
|
endpoint: "/whatsapp/webhook",
|
31
31
|
icon: "💬",
|
32
32
|
color: "#25D366"
|
33
33
|
},
|
34
|
-
whatsapp_v2: {
|
35
|
-
name: "WhatsApp API v2",
|
36
|
-
description: "Alternative WhatsApp endpoint (v2 API)",
|
37
|
-
processor_type: "whatsapp",
|
38
|
-
provider: "cloud_api",
|
39
|
-
endpoint: "/api/v2/whatsapp/webhook",
|
40
|
-
icon: "🔄",
|
41
|
-
color: "#17a2b8"
|
42
|
-
},
|
43
34
|
whatsapp_tenant_a: {
|
44
35
|
name: "Tenant A WhatsApp",
|
45
|
-
description: "Multi-tenant
|
36
|
+
description: "Multi-tenant endpoint for Tenant A",
|
46
37
|
processor_type: "whatsapp",
|
47
38
|
provider: "cloud_api",
|
48
39
|
endpoint: "/tenants/a/whatsapp/webhook",
|
@@ -51,7 +42,7 @@ class SimulatorController < ApplicationController
|
|
51
42
|
},
|
52
43
|
whatsapp_legacy: {
|
53
44
|
name: "Legacy WhatsApp",
|
54
|
-
description: "Legacy
|
45
|
+
description: "Legacy endpoint for compatibility",
|
55
46
|
processor_type: "whatsapp",
|
56
47
|
provider: "cloud_api",
|
57
48
|
endpoint: "/legacy/whatsapp",
|
@@ -71,7 +62,7 @@ class SimulatorController < ApplicationController
|
|
71
62
|
"+1234567890"
|
72
63
|
end
|
73
64
|
|
74
|
-
# Default test contact name
|
65
|
+
# Default test contact name
|
75
66
|
def default_contact_name
|
76
67
|
"Test User"
|
77
68
|
end
|
@@ -90,6 +81,6 @@ end
|
|
90
81
|
# This allows you to test:
|
91
82
|
# - Different controller implementations on the same server
|
92
83
|
# - Different API versions (v1, v2, etc.)
|
93
|
-
# - Multi-tenant endpoints with different configurations
|
84
|
+
# - Multi-tenant endpoints with different configurations
|
94
85
|
# - Legacy endpoints alongside new ones
|
95
|
-
# - Different flow implementations for different endpoints
|
86
|
+
# - Different flow implementations for different endpoints
|