flow_chat 0.2.1 โ 0.4.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/.ruby-version +1 -0
- data/Gemfile +3 -1
- data/README.md +784 -63
- data/Rakefile +7 -3
- data/examples/initializer.rb +31 -0
- data/examples/multi_tenant_whatsapp_controller.rb +248 -0
- data/examples/ussd_controller.rb +264 -0
- data/examples/whatsapp_controller.rb +141 -0
- data/lib/flow_chat/base_processor.rb +63 -0
- data/lib/flow_chat/config.rb +21 -8
- data/lib/flow_chat/context.rb +8 -0
- data/lib/flow_chat/interrupt.rb +2 -0
- data/lib/flow_chat/session/cache_session_store.rb +84 -0
- data/lib/flow_chat/session/middleware.rb +15 -7
- data/lib/flow_chat/ussd/app.rb +25 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +3 -1
- data/lib/flow_chat/ussd/gateway/nsano.rb +7 -1
- data/lib/flow_chat/ussd/middleware/pagination.rb +14 -17
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +18 -31
- data/lib/flow_chat/ussd/processor.rb +15 -42
- data/lib/flow_chat/ussd/prompt.rb +1 -1
- data/lib/flow_chat/ussd/simulator/controller.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +58 -0
- data/lib/flow_chat/whatsapp/configuration.rb +75 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +213 -0
- data/lib/flow_chat/whatsapp/middleware/executor.rb +30 -0
- data/lib/flow_chat/whatsapp/processor.rb +36 -0
- data/lib/flow_chat/whatsapp/prompt.rb +206 -0
- data/lib/flow_chat/whatsapp/template_manager.rb +162 -0
- data/lib/flow_chat.rb +1 -0
- metadata +17 -4
- data/.rspec +0 -3
data/README.md
CHANGED
@@ -1,85 +1,702 @@
|
|
1
1
|
# FlowChat
|
2
2
|
|
3
|
-
FlowChat is a Rails framework designed for
|
3
|
+
FlowChat is a Rails framework designed for building sophisticated conversational workflows for both USSD (Unstructured Supplementary Service Data) systems and WhatsApp messaging. It provides an intuitive Ruby DSL for creating multi-step, menu-driven conversations with automatic session management, input validation, and flow control.
|
4
4
|
|
5
|
-
|
5
|
+
**Key Features:**
|
6
|
+
- ๐ฏ **Declarative Flow Definition** - Define conversation flows as Ruby classes
|
7
|
+
- ๐ **Automatic Session Management** - Persistent state across requests
|
8
|
+
- โ
**Input Validation & Transformation** - Built-in validation and data conversion
|
9
|
+
- ๐ **Middleware Architecture** - Flexible request processing pipeline
|
10
|
+
- ๐ฑ **USSD Gateway Support** - Currently supports Nalo gateways
|
11
|
+
- ๐ฌ **WhatsApp Integration** - Full WhatsApp Cloud API support with interactive messages
|
12
|
+
- ๐งช **Built-in Testing Tools** - USSD simulator for local development
|
6
13
|
|
7
|
-
##
|
14
|
+
## Architecture Overview
|
8
15
|
|
9
|
-
|
16
|
+
FlowChat uses a **request-per-interaction** model where each user input creates a new request. The framework maintains conversation state through session storage while processing each interaction through a middleware pipeline.
|
10
17
|
|
11
|
-
|
18
|
+
```
|
19
|
+
User Input โ Gateway โ Session โ Pagination โ Custom โ Executor โ Flow โ Response
|
20
|
+
โ
|
21
|
+
Session Storage
|
22
|
+
```
|
23
|
+
|
24
|
+
**Middleware Pipeline:**
|
25
|
+
- **Gateway**: Communication with providers (USSD: Nalo, WhatsApp: Cloud API)
|
26
|
+
- **Session**: Load/save conversation state
|
27
|
+
- **Pagination**: Split long responses into pages (USSD only)
|
28
|
+
- **Custom**: Your application middleware (logging, auth, etc.)
|
29
|
+
- **Executor**: Execute flow methods and handle interrupts
|
30
|
+
|
31
|
+
## Installation
|
32
|
+
|
33
|
+
Add FlowChat to your Rails application's Gemfile:
|
12
34
|
|
13
35
|
```ruby
|
14
|
-
gem 'flow_chat'
|
36
|
+
gem 'flow_chat'
|
15
37
|
```
|
16
38
|
|
17
|
-
Then
|
39
|
+
Then execute:
|
18
40
|
|
19
41
|
```bash
|
20
42
|
bundle install
|
21
43
|
```
|
22
44
|
|
23
|
-
|
45
|
+
## Quick Start
|
46
|
+
|
47
|
+
FlowChat supports both USSD and WhatsApp. Choose the platform that fits your needs:
|
48
|
+
|
49
|
+
### USSD Setup
|
50
|
+
|
51
|
+
### 1. Create Your First Flow
|
52
|
+
|
53
|
+
Create a flow class in `app/flow_chat/welcome_flow.rb`:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class WelcomeFlow < FlowChat::Flow
|
57
|
+
def main_page
|
58
|
+
name = app.screen(:name) do |prompt|
|
59
|
+
prompt.ask "Welcome! What's your name?",
|
60
|
+
transform: ->(input) { input.strip.titleize }
|
61
|
+
end
|
62
|
+
|
63
|
+
app.say "Hello, #{name}! Welcome to FlowChat."
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
### 2. Set Up the USSD Controller
|
69
|
+
|
70
|
+
Create a controller to handle USSD requests:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class UssdController < ApplicationController
|
74
|
+
skip_forgery_protection
|
75
|
+
|
76
|
+
def process_request
|
77
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
78
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
79
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
80
|
+
end
|
81
|
+
|
82
|
+
processor.run WelcomeFlow, :main_page
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
### 3. Configure Routes
|
88
|
+
|
89
|
+
Add the route to `config/routes.rb`:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
Rails.application.routes.draw do
|
93
|
+
post 'ussd' => 'ussd#process_request'
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
๐ก **Tip**: See [examples/ussd_controller.rb](examples/ussd_controller.rb) for a complete USSD controller example with payment flows, customer support, and custom middleware.
|
98
|
+
|
99
|
+
### WhatsApp Setup
|
100
|
+
|
101
|
+
### 1. Configure WhatsApp Credentials
|
102
|
+
|
103
|
+
FlowChat supports two ways to configure WhatsApp credentials:
|
104
|
+
|
105
|
+
**Option A: Using Rails Credentials**
|
106
|
+
|
107
|
+
Add your WhatsApp credentials to Rails credentials:
|
108
|
+
|
109
|
+
```bash
|
110
|
+
rails credentials:edit
|
111
|
+
```
|
112
|
+
|
113
|
+
```yaml
|
114
|
+
whatsapp:
|
115
|
+
access_token: "your_access_token"
|
116
|
+
phone_number_id: "your_phone_number_id"
|
117
|
+
verify_token: "your_verify_token"
|
118
|
+
app_id: "your_app_id"
|
119
|
+
app_secret: "your_app_secret"
|
120
|
+
webhook_url: "your_webhook_url"
|
121
|
+
business_account_id: "your_business_account_id"
|
122
|
+
```
|
123
|
+
|
124
|
+
**Option B: Using Environment Variables**
|
125
|
+
|
126
|
+
Alternatively, you can use environment variables:
|
24
127
|
|
25
128
|
```bash
|
26
|
-
|
129
|
+
# Add to your .env file or environment
|
130
|
+
export WHATSAPP_ACCESS_TOKEN="your_access_token"
|
131
|
+
export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
|
132
|
+
export WHATSAPP_VERIFY_TOKEN="your_verify_token"
|
133
|
+
export WHATSAPP_APP_ID="your_app_id"
|
134
|
+
export WHATSAPP_APP_SECRET="your_app_secret"
|
135
|
+
export WHATSAPP_WEBHOOK_URL="your_webhook_url"
|
136
|
+
export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
|
27
137
|
```
|
28
138
|
|
29
|
-
|
139
|
+
FlowChat will automatically use Rails credentials first, falling back to environment variables if credentials are not available.
|
30
140
|
|
31
|
-
|
141
|
+
**Option C: Per-Setup Configuration**
|
32
142
|
|
33
|
-
|
143
|
+
For multi-tenant applications or when you need different WhatsApp accounts per endpoint:
|
34
144
|
|
35
|
-
|
145
|
+
```ruby
|
146
|
+
# Create custom configuration
|
147
|
+
custom_config = FlowChat::Whatsapp::Configuration.new
|
148
|
+
custom_config.access_token = "your_specific_access_token"
|
149
|
+
custom_config.phone_number_id = "your_specific_phone_number_id"
|
150
|
+
custom_config.verify_token = "your_specific_verify_token"
|
151
|
+
custom_config.app_id = "your_specific_app_id"
|
152
|
+
custom_config.app_secret = "your_specific_app_secret"
|
153
|
+
custom_config.business_account_id = "your_specific_business_account_id"
|
154
|
+
|
155
|
+
# Use in processor
|
156
|
+
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
157
|
+
config.use_whatsapp_config(custom_config) # Pass custom config
|
158
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
159
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
๐ก **Tip**: See [examples/multi_tenant_whatsapp_controller.rb](examples/multi_tenant_whatsapp_controller.rb) for comprehensive multi-tenant and per-setup configuration examples.
|
164
|
+
|
165
|
+
### 2. Create WhatsApp Controller
|
36
166
|
|
37
167
|
```ruby
|
38
|
-
class
|
168
|
+
class WhatsappController < ApplicationController
|
169
|
+
skip_forgery_protection
|
170
|
+
|
171
|
+
def webhook
|
172
|
+
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
173
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
174
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
175
|
+
end
|
176
|
+
|
177
|
+
processor.run WelcomeFlow, :main_page
|
178
|
+
end
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
### 3. Add WhatsApp Route
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
Rails.application.routes.draw do
|
186
|
+
match '/whatsapp/webhook', to: 'whatsapp#webhook', via: [:get, :post]
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
### 4. Enhanced Features for WhatsApp
|
191
|
+
|
192
|
+
The same flow works for both USSD and WhatsApp, but WhatsApp provides additional data and better interactive features:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
class WelcomeFlow < FlowChat::Flow
|
39
196
|
def main_page
|
40
|
-
|
197
|
+
# Access WhatsApp-specific data
|
198
|
+
Rails.logger.info "Contact: #{app.contact_name}, Phone: #{app.phone_number}"
|
199
|
+
Rails.logger.info "Message ID: #{app.message_id}, Timestamp: #{app.timestamp}"
|
200
|
+
|
201
|
+
# Handle location sharing
|
202
|
+
if app.location
|
203
|
+
app.say "Thanks for sharing your location! We see you're at #{app.location['latitude']}, #{app.location['longitude']}"
|
204
|
+
return
|
205
|
+
end
|
206
|
+
|
207
|
+
# Handle media messages
|
208
|
+
if app.media
|
209
|
+
app.say "Thanks for the #{app.media['type']} file! We received: #{app.media['id']}"
|
210
|
+
return
|
211
|
+
end
|
212
|
+
|
213
|
+
name = app.screen(:name) do |prompt|
|
214
|
+
prompt.ask "Hello! Welcome to our WhatsApp service. What's your name?",
|
215
|
+
transform: ->(input) { input.strip.titleize }
|
216
|
+
end
|
217
|
+
|
218
|
+
# WhatsApp supports interactive buttons and lists via prompt.select
|
219
|
+
choice = app.screen(:main_menu) do |prompt|
|
220
|
+
prompt.select "Hi #{name}! How can I help you?", {
|
221
|
+
"info" => "๐ Get Information",
|
222
|
+
"support" => "๐ Contact Support",
|
223
|
+
"feedback" => "๐ฌ Give Feedback"
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
case choice
|
228
|
+
when "info"
|
229
|
+
show_information_menu
|
230
|
+
when "support"
|
231
|
+
contact_support
|
232
|
+
when "feedback"
|
233
|
+
collect_feedback
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def show_information_menu
|
240
|
+
info_choice = app.screen(:info_menu) do |prompt|
|
241
|
+
prompt.select "What information do you need?", {
|
242
|
+
"hours" => "๐ Business Hours",
|
243
|
+
"location" => "๐ Our Location",
|
244
|
+
"services" => "๐ผ Our Services"
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
case info_choice
|
249
|
+
when "hours"
|
250
|
+
app.say "We're open Monday-Friday 9AM-6PM, Saturday 10AM-4PM. Closed Sundays."
|
251
|
+
when "location"
|
252
|
+
app.say "๐ Visit us at 123 Main Street, Downtown. We're next to the coffee shop!"
|
253
|
+
when "services"
|
254
|
+
app.say "๐ผ We offer: Web Development, Mobile Apps, Cloud Services, and IT Consulting."
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def contact_support
|
259
|
+
support_choice = app.screen(:support_menu) do |prompt|
|
260
|
+
prompt.select "How would you like to contact support?", {
|
261
|
+
"call" => "๐ Call Us",
|
262
|
+
"email" => "๐ง Email Us",
|
263
|
+
"chat" => "๐ฌ Continue Here"
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
case support_choice
|
268
|
+
when "call"
|
269
|
+
app.say "๐ Call us at: +1-555-HELP (4357)\nAvailable Mon-Fri 9AM-5PM"
|
270
|
+
when "email"
|
271
|
+
app.say "๐ง Email us at: support@company.com\nWe typically respond within 24 hours"
|
272
|
+
when "chat"
|
273
|
+
app.say "๐ฌ Great! Please describe your issue and we'll help you right away."
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def collect_feedback
|
278
|
+
rating = app.screen(:rating) do |prompt|
|
279
|
+
prompt.select "How would you rate our service?", {
|
280
|
+
"5" => "โญโญโญโญโญ Excellent",
|
281
|
+
"4" => "โญโญโญโญ Good",
|
282
|
+
"3" => "โญโญโญ Average",
|
283
|
+
"2" => "โญโญ Poor",
|
284
|
+
"1" => "โญ Very Poor"
|
285
|
+
}
|
286
|
+
end
|
287
|
+
|
288
|
+
feedback = app.screen(:feedback_text) do |prompt|
|
289
|
+
prompt.ask "Thank you for the #{rating}-star rating! Please share any additional feedback:"
|
290
|
+
end
|
291
|
+
|
292
|
+
# Use WhatsApp-specific data for logging
|
293
|
+
Rails.logger.info "Feedback from #{app.contact_name} (#{app.phone_number}): #{rating} stars - #{feedback}"
|
294
|
+
|
295
|
+
app.say "Thank you for your feedback! We really appreciate it. ๐"
|
41
296
|
end
|
42
297
|
end
|
43
298
|
```
|
44
299
|
|
45
|
-
|
300
|
+
For detailed WhatsApp setup instructions, see [WhatsApp Integration Guide](docs/whatsapp_setup.md).
|
301
|
+
|
302
|
+
## Cross-Platform Compatibility
|
46
303
|
|
47
|
-
|
304
|
+
FlowChat provides a unified API that works across both USSD and WhatsApp platforms, with graceful degradation for platform-specific features:
|
48
305
|
|
49
|
-
|
306
|
+
### Shared Features (Both USSD & WhatsApp)
|
307
|
+
- โ
`app.screen()` - Interactive screens with prompts
|
308
|
+
- โ
`app.say()` - Send messages to users
|
309
|
+
- โ
`prompt.ask()` - Text input collection
|
310
|
+
- โ
`prompt.select()` - Menu selection (renders as numbered list in USSD, interactive buttons/lists in WhatsApp)
|
311
|
+
- โ
`prompt.yes?()` - Yes/no questions
|
312
|
+
- โ
`app.phone_number` - User's phone number
|
313
|
+
- โ
`app.message_id` - Unique message identifier
|
314
|
+
- โ
`app.timestamp` - Message timestamp
|
315
|
+
|
316
|
+
### WhatsApp-Only Features
|
317
|
+
- โ
`app.contact_name` - WhatsApp contact name (returns `nil` in USSD)
|
318
|
+
- โ
`app.location` - Location sharing data (returns `nil` in USSD)
|
319
|
+
- โ
`app.media` - Media file attachments (returns `nil` in USSD)
|
320
|
+
- โ
Rich interactive elements (buttons, lists) automatically generated from `prompt.select()`
|
321
|
+
|
322
|
+
This design allows you to write flows once and deploy them on both platforms, with WhatsApp users getting enhanced interactive features automatically.
|
323
|
+
|
324
|
+
## Core Concepts
|
325
|
+
|
326
|
+
### Flows and Screens
|
327
|
+
|
328
|
+
**Flows** are Ruby classes that define conversation logic. **Screens** represent individual interaction points where you collect user input.
|
50
329
|
|
51
330
|
```ruby
|
52
|
-
class
|
53
|
-
|
331
|
+
class RegistrationFlow < FlowChat::Flow
|
332
|
+
def main_page
|
333
|
+
# Each screen captures one piece of user input
|
334
|
+
phone = app.screen(:phone) do |prompt|
|
335
|
+
prompt.ask "Enter your phone number:",
|
336
|
+
validate: ->(input) { "Invalid phone number" unless valid_phone?(input) }
|
337
|
+
end
|
54
338
|
|
55
|
-
|
56
|
-
|
339
|
+
age = app.screen(:age) do |prompt|
|
340
|
+
prompt.ask "Enter your age:",
|
341
|
+
convert: ->(input) { input.to_i },
|
342
|
+
validate: ->(input) { "Must be 18 or older" unless input >= 18 }
|
343
|
+
end
|
344
|
+
|
345
|
+
# Process the collected data
|
346
|
+
create_user(phone: phone, age: age)
|
347
|
+
app.say "Registration complete!"
|
57
348
|
end
|
58
349
|
|
59
350
|
private
|
60
351
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
352
|
+
def valid_phone?(phone)
|
353
|
+
phone.match?(/\A\+?[\d\s\-\(\)]+\z/)
|
354
|
+
end
|
355
|
+
|
356
|
+
def create_user(phone:, age:)
|
357
|
+
# Your user creation logic here
|
358
|
+
end
|
359
|
+
end
|
360
|
+
```
|
361
|
+
|
362
|
+
### Input Validation and Transformation
|
363
|
+
|
364
|
+
FlowChat provides powerful input processing capabilities:
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
app.screen(:email) do |prompt|
|
368
|
+
prompt.ask "Enter your email:",
|
369
|
+
# Transform input before validation
|
370
|
+
transform: ->(input) { input.strip.downcase },
|
371
|
+
|
372
|
+
# Validate the input
|
373
|
+
validate: ->(input) {
|
374
|
+
"Invalid email format" unless input.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
375
|
+
},
|
376
|
+
|
377
|
+
# Convert to final format
|
378
|
+
convert: ->(input) { input }
|
379
|
+
end
|
380
|
+
```
|
381
|
+
|
382
|
+
### Menu Selection
|
383
|
+
|
384
|
+
Create selection menus with automatic validation:
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
# Array-based choices
|
388
|
+
language = app.screen(:language) do |prompt|
|
389
|
+
prompt.select "Choose your language:", ["English", "French", "Spanish"]
|
390
|
+
end
|
391
|
+
|
392
|
+
# Hash-based choices (keys are returned values)
|
393
|
+
plan = app.screen(:plan) do |prompt|
|
394
|
+
prompt.select "Choose a plan:", {
|
395
|
+
"basic" => "Basic Plan ($10/month)",
|
396
|
+
"premium" => "Premium Plan ($25/month)",
|
397
|
+
"enterprise" => "Enterprise Plan ($100/month)"
|
398
|
+
}
|
399
|
+
end
|
400
|
+
```
|
401
|
+
|
402
|
+
### Yes/No Prompts
|
403
|
+
|
404
|
+
Simplified boolean input collection:
|
405
|
+
|
406
|
+
```ruby
|
407
|
+
confirmed = app.screen(:confirmation) do |prompt|
|
408
|
+
prompt.yes? "Do you want to proceed with the payment?"
|
409
|
+
end
|
410
|
+
|
411
|
+
if confirmed
|
412
|
+
process_payment
|
413
|
+
app.say "Payment processed successfully!"
|
414
|
+
else
|
415
|
+
app.say "Payment cancelled."
|
416
|
+
end
|
417
|
+
```
|
418
|
+
|
419
|
+
## Advanced Features
|
420
|
+
|
421
|
+
### Session Management and Flow State
|
422
|
+
|
423
|
+
FlowChat automatically manages session state across requests. Each screen's result is cached, so users can navigate back and forth without losing data:
|
424
|
+
|
425
|
+
```ruby
|
426
|
+
class OrderFlow < FlowChat::Flow
|
427
|
+
def main_page
|
428
|
+
# These values persist across requests
|
429
|
+
product = app.screen(:product) { |p| p.select "Choose product:", products }
|
430
|
+
quantity = app.screen(:quantity) { |p| p.ask "Quantity:", convert: :to_i }
|
431
|
+
|
432
|
+
# Show summary
|
433
|
+
total = calculate_total(product, quantity)
|
434
|
+
confirmed = app.screen(:confirm) do |prompt|
|
435
|
+
prompt.yes? "Order #{quantity}x #{product} for $#{total}. Confirm?"
|
436
|
+
end
|
437
|
+
|
438
|
+
if confirmed
|
439
|
+
process_order(product, quantity)
|
440
|
+
app.say "Order placed successfully!"
|
441
|
+
else
|
442
|
+
app.say "Order cancelled."
|
65
443
|
end
|
66
444
|
end
|
67
445
|
end
|
68
446
|
```
|
69
447
|
|
70
|
-
|
448
|
+
### Error Handling
|
71
449
|
|
72
|
-
|
450
|
+
Handle validation errors gracefully:
|
73
451
|
|
74
452
|
```ruby
|
75
|
-
|
76
|
-
|
453
|
+
app.screen(:credit_card) do |prompt|
|
454
|
+
prompt.ask "Enter credit card number:",
|
455
|
+
validate: ->(input) {
|
456
|
+
return "Card number must be 16 digits" unless input.length == 16
|
457
|
+
return "Invalid card number" unless luhn_valid?(input)
|
458
|
+
nil # Return nil for valid input
|
459
|
+
}
|
460
|
+
end
|
461
|
+
```
|
462
|
+
|
463
|
+
### Middleware Configuration
|
464
|
+
|
465
|
+
FlowChat uses a **middleware architecture** to process USSD requests through a configurable pipeline. Each request flows through multiple middleware layers in a specific order.
|
466
|
+
|
467
|
+
#### Default Middleware Stack
|
468
|
+
|
469
|
+
When you run a flow, FlowChat automatically builds this middleware stack:
|
470
|
+
|
471
|
+
```
|
472
|
+
User Input โ Gateway โ Session โ Pagination โ Custom Middleware โ Executor โ Flow
|
473
|
+
```
|
474
|
+
|
475
|
+
1. **Gateway Middleware** - Handles USSD provider communication (Nalo)
|
476
|
+
2. **Session Middleware** - Manages session storage and retrieval
|
477
|
+
3. **Pagination Middleware** - Automatically splits long responses across pages
|
478
|
+
4. **Custom Middleware** - Your application-specific middleware (optional)
|
479
|
+
5. **Executor Middleware** - Executes the actual flow logic
|
480
|
+
|
481
|
+
#### Basic Configuration
|
482
|
+
|
483
|
+
```ruby
|
484
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
485
|
+
# Gateway configuration (required)
|
486
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
487
|
+
|
488
|
+
# Session storage (required)
|
489
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
490
|
+
|
491
|
+
# Add custom middleware (optional)
|
492
|
+
config.use_middleware MyLoggingMiddleware
|
493
|
+
|
494
|
+
# Enable resumable sessions (optional)
|
495
|
+
config.use_resumable_sessions
|
77
496
|
end
|
78
497
|
```
|
79
498
|
|
80
|
-
####
|
499
|
+
#### Runtime Middleware Modification
|
81
500
|
|
82
|
-
|
501
|
+
You can modify the middleware stack at runtime for advanced use cases:
|
502
|
+
|
503
|
+
```ruby
|
504
|
+
processor.run(MyFlow, :main_page) do |stack|
|
505
|
+
# Add authentication middleware
|
506
|
+
stack.use AuthenticationMiddleware
|
507
|
+
|
508
|
+
# Insert rate limiting before execution
|
509
|
+
stack.insert_before FlowChat::Ussd::Middleware::Executor, RateLimitMiddleware
|
510
|
+
|
511
|
+
# Add logging after gateway
|
512
|
+
stack.insert_after gateway, RequestLoggingMiddleware
|
513
|
+
end
|
514
|
+
```
|
515
|
+
|
516
|
+
#### Built-in Middleware
|
517
|
+
|
518
|
+
**Pagination Middleware** automatically handles responses longer than 182 characters (configurable):
|
519
|
+
|
520
|
+
```ruby
|
521
|
+
# Configure pagination behavior
|
522
|
+
FlowChat::Config.ussd.pagination_page_size = 140 # Default: 140 characters
|
523
|
+
FlowChat::Config.ussd.pagination_next_option = "#" # Default: "#"
|
524
|
+
FlowChat::Config.ussd.pagination_next_text = "More" # Default: "More"
|
525
|
+
FlowChat::Config.ussd.pagination_back_option = "0" # Default: "0"
|
526
|
+
FlowChat::Config.ussd.pagination_back_text = "Back" # Default: "Back"
|
527
|
+
```
|
528
|
+
|
529
|
+
**Resumable Sessions** allow users to continue interrupted conversations:
|
530
|
+
|
531
|
+
```ruby
|
532
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
533
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
534
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
535
|
+
config.use_resumable_sessions # Enable resumable sessions
|
536
|
+
end
|
537
|
+
```
|
538
|
+
|
539
|
+
#### Creating Custom Middleware
|
540
|
+
|
541
|
+
```ruby
|
542
|
+
class LoggingMiddleware
|
543
|
+
def initialize(app)
|
544
|
+
@app = app
|
545
|
+
end
|
546
|
+
|
547
|
+
def call(context)
|
548
|
+
Rails.logger.info "Processing USSD request: #{context.input}"
|
549
|
+
|
550
|
+
# Call the next middleware in the stack
|
551
|
+
result = @app.call(context)
|
552
|
+
|
553
|
+
Rails.logger.info "Response: #{result[1]}"
|
554
|
+
result
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
# Use your custom middleware
|
559
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
560
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
561
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
562
|
+
config.use_middleware LoggingMiddleware
|
563
|
+
end
|
564
|
+
```
|
565
|
+
|
566
|
+
### Multiple Gateways
|
567
|
+
|
568
|
+
FlowChat supports multiple USSD gateways:
|
569
|
+
|
570
|
+
```ruby
|
571
|
+
# Nalo Solutions Gateway
|
572
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
573
|
+
```
|
574
|
+
|
575
|
+
## Testing
|
576
|
+
|
577
|
+
### Unit Testing Flows
|
578
|
+
|
579
|
+
Test your flows in isolation using the provided test helpers:
|
580
|
+
|
581
|
+
```ruby
|
582
|
+
require 'test_helper'
|
583
|
+
|
584
|
+
class WelcomeFlowTest < Minitest::Test
|
585
|
+
def setup
|
586
|
+
@context = FlowChat::Context.new
|
587
|
+
@context.session = create_test_session_store
|
588
|
+
end
|
589
|
+
|
590
|
+
def test_welcome_flow_with_name
|
591
|
+
@context.input = "John Doe"
|
592
|
+
app = FlowChat::Ussd::App.new(@context)
|
593
|
+
|
594
|
+
error = assert_raises(FlowChat::Interrupt::Terminate) do
|
595
|
+
flow = WelcomeFlow.new(app)
|
596
|
+
flow.main_page
|
597
|
+
end
|
598
|
+
|
599
|
+
assert_equal "Hello, John Doe! Welcome to FlowChat.", error.prompt
|
600
|
+
end
|
601
|
+
|
602
|
+
def test_welcome_flow_without_input
|
603
|
+
@context.input = nil
|
604
|
+
app = FlowChat::Ussd::App.new(@context)
|
605
|
+
|
606
|
+
error = assert_raises(FlowChat::Interrupt::Prompt) do
|
607
|
+
flow = WelcomeFlow.new(app)
|
608
|
+
flow.main_page
|
609
|
+
end
|
610
|
+
|
611
|
+
assert_equal "Welcome! What's your name?", error.prompt
|
612
|
+
end
|
613
|
+
end
|
614
|
+
```
|
615
|
+
|
616
|
+
### Integration Testing
|
617
|
+
|
618
|
+
Test complete user journeys:
|
619
|
+
|
620
|
+
```ruby
|
621
|
+
class RegistrationFlowIntegrationTest < Minitest::Test
|
622
|
+
def test_complete_registration_flow
|
623
|
+
controller = mock_controller
|
624
|
+
processor = FlowChat::Ussd::Processor.new(controller) do |config|
|
625
|
+
config.use_gateway MockGateway
|
626
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
627
|
+
end
|
628
|
+
|
629
|
+
# Simulate the complete flow
|
630
|
+
# First request - ask for phone
|
631
|
+
# Second request - provide phone, ask for age
|
632
|
+
# Third request - provide age, complete registration
|
633
|
+
end
|
634
|
+
end
|
635
|
+
```
|
636
|
+
|
637
|
+
### Testing Middleware
|
638
|
+
|
639
|
+
Test your custom middleware in isolation:
|
640
|
+
|
641
|
+
```ruby
|
642
|
+
class LoggingMiddlewareTest < Minitest::Test
|
643
|
+
def test_logs_request_and_response
|
644
|
+
# Mock the next app in the chain
|
645
|
+
app = lambda { |context| [:prompt, "Test response", []] }
|
646
|
+
middleware = LoggingMiddleware.new(app)
|
647
|
+
|
648
|
+
context = FlowChat::Context.new
|
649
|
+
context.input = "test input"
|
650
|
+
|
651
|
+
# Capture log output
|
652
|
+
log_output = StringIO.new
|
653
|
+
Rails.stub(:logger, Logger.new(log_output)) do
|
654
|
+
type, prompt, choices = middleware.call(context)
|
655
|
+
|
656
|
+
assert_equal :prompt, type
|
657
|
+
assert_equal "Test response", prompt
|
658
|
+
assert_includes log_output.string, "Processing USSD request: test input"
|
659
|
+
assert_includes log_output.string, "Response: Test response"
|
660
|
+
end
|
661
|
+
end
|
662
|
+
end
|
663
|
+
```
|
664
|
+
|
665
|
+
### Testing Middleware Stack Modification
|
666
|
+
|
667
|
+
Test runtime middleware modifications:
|
668
|
+
|
669
|
+
```ruby
|
670
|
+
class ProcessorMiddlewareTest < Minitest::Test
|
671
|
+
def test_custom_middleware_insertion
|
672
|
+
controller = mock_controller
|
673
|
+
processor = FlowChat::Ussd::Processor.new(controller) do |config|
|
674
|
+
config.use_gateway MockGateway
|
675
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
676
|
+
end
|
677
|
+
|
678
|
+
custom_middleware_called = false
|
679
|
+
custom_middleware = Class.new do
|
680
|
+
define_method(:initialize) { |app| @app = app }
|
681
|
+
define_method(:call) do |context|
|
682
|
+
custom_middleware_called = true
|
683
|
+
@app.call(context)
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
processor.run(TestFlow, :main_page) do |stack|
|
688
|
+
stack.use custom_middleware
|
689
|
+
stack.insert_before FlowChat::Ussd::Middleware::Executor, custom_middleware
|
690
|
+
end
|
691
|
+
|
692
|
+
assert custom_middleware_called, "Custom middleware should have been executed"
|
693
|
+
end
|
694
|
+
end
|
695
|
+
```
|
696
|
+
|
697
|
+
### USSD Simulator
|
698
|
+
|
699
|
+
Use the built-in simulator for interactive testing:
|
83
700
|
|
84
701
|
```ruby
|
85
702
|
class UssdSimulatorController < ApplicationController
|
@@ -88,7 +705,7 @@ class UssdSimulatorController < ApplicationController
|
|
88
705
|
protected
|
89
706
|
|
90
707
|
def default_endpoint
|
91
|
-
'/
|
708
|
+
'/ussd'
|
92
709
|
end
|
93
710
|
|
94
711
|
def default_provider
|
@@ -97,62 +714,166 @@ class UssdSimulatorController < ApplicationController
|
|
97
714
|
end
|
98
715
|
```
|
99
716
|
|
100
|
-
|
717
|
+
Add to routes and visit `http://localhost:3000/ussd_simulator`.
|
718
|
+
|
719
|
+
## Best Practices
|
720
|
+
|
721
|
+
### 1. Keep Flows Focused
|
722
|
+
|
723
|
+
Create separate flows for different user journeys:
|
101
724
|
|
102
725
|
```ruby
|
103
|
-
|
104
|
-
|
726
|
+
# Good: Focused flows
|
727
|
+
class LoginFlow < FlowChat::Flow
|
728
|
+
# Handle user authentication
|
729
|
+
end
|
730
|
+
|
731
|
+
class RegistrationFlow < FlowChat::Flow
|
732
|
+
# Handle user registration
|
733
|
+
end
|
734
|
+
|
735
|
+
class AccountFlow < FlowChat::Flow
|
736
|
+
# Handle account management
|
105
737
|
end
|
106
738
|
```
|
107
739
|
|
108
|
-
|
740
|
+
### 2. Use Descriptive Screen Names
|
741
|
+
|
742
|
+
Screen names should clearly indicate their purpose:
|
743
|
+
|
744
|
+
```ruby
|
745
|
+
# Good
|
746
|
+
app.screen(:customer_phone_number) { |p| p.ask "Phone:" }
|
747
|
+
app.screen(:payment_confirmation) { |p| p.yes? "Confirm payment?" }
|
748
|
+
|
749
|
+
# Avoid
|
750
|
+
app.screen(:input1) { |p| p.ask "Phone:" }
|
751
|
+
app.screen(:confirm) { |p| p.yes? "Confirm payment?" }
|
752
|
+
```
|
109
753
|
|
110
|
-
###
|
754
|
+
### 3. Validate Early and Often
|
111
755
|
|
112
|
-
|
756
|
+
Always validate user input to provide clear feedback:
|
113
757
|
|
114
758
|
```ruby
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
759
|
+
app.screen(:amount) do |prompt|
|
760
|
+
prompt.ask "Enter amount:",
|
761
|
+
convert: ->(input) { input.to_f },
|
762
|
+
validate: ->(amount) {
|
763
|
+
return "Amount must be positive" if amount <= 0
|
764
|
+
return "Maximum amount is $1000" if amount > 1000
|
765
|
+
nil
|
119
766
|
}
|
767
|
+
end
|
768
|
+
```
|
120
769
|
|
121
|
-
|
122
|
-
prompt.ask "How old are you?",
|
123
|
-
convert: ->(input) { input.to_i },
|
124
|
-
validate: ->(input) { "You must be at least 13 years old" unless input >= 13 }
|
125
|
-
end
|
126
|
-
|
127
|
-
gender = app.screen(:gender) { |prompt| prompt.select "What is your gender", ["Male", "Female"] }
|
770
|
+
### 4. Handle Edge Cases
|
128
771
|
|
129
|
-
|
130
|
-
prompt.yes?("Is this correct?\n\nName: #{name}\nAge: #{age}\nGender: #{gender}")
|
131
|
-
end
|
772
|
+
Consider error scenarios and provide helpful messages:
|
132
773
|
|
133
|
-
|
774
|
+
```ruby
|
775
|
+
def main_page
|
776
|
+
begin
|
777
|
+
process_user_request
|
778
|
+
rescue PaymentError => e
|
779
|
+
app.say "Payment failed: #{e.message}. Please try again."
|
780
|
+
rescue SystemError
|
781
|
+
app.say "System temporarily unavailable. Please try again later."
|
134
782
|
end
|
135
783
|
end
|
136
784
|
```
|
137
785
|
|
138
|
-
|
786
|
+
## Configuration
|
787
|
+
|
788
|
+
### Cache Configuration
|
789
|
+
|
790
|
+
The `CacheSessionStore` requires a cache to be configured. Set it up in your Rails application:
|
791
|
+
|
792
|
+
```ruby
|
793
|
+
# config/application.rb or config/environments/*.rb
|
794
|
+
FlowChat::Config.cache = Rails.cache
|
795
|
+
|
796
|
+
# Or use a specific cache store
|
797
|
+
FlowChat::Config.cache = ActiveSupport::Cache::MemoryStore.new
|
798
|
+
|
799
|
+
# For Redis (requires redis gem)
|
800
|
+
FlowChat::Config.cache = ActiveSupport::Cache::RedisCacheStore.new(url: "redis://localhost:6379/1")
|
801
|
+
```
|
802
|
+
|
803
|
+
๐ก **Tip**: See [examples/initializer.rb](examples/initializer.rb) for a complete configuration example.
|
804
|
+
|
805
|
+
### Session Storage Options
|
139
806
|
|
140
|
-
|
807
|
+
Configure different session storage backends:
|
141
808
|
|
142
|
-
|
809
|
+
```ruby
|
810
|
+
# Cache session store (default) - uses FlowChat::Config.cache
|
811
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
143
812
|
|
144
|
-
|
813
|
+
# Rails session (for USSD)
|
814
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
815
|
+
|
816
|
+
# Custom session store
|
817
|
+
class MySessionStore
|
818
|
+
def initialize(context)
|
819
|
+
@context = context
|
820
|
+
end
|
821
|
+
|
822
|
+
def get(key)
|
823
|
+
# Your storage logic
|
824
|
+
end
|
825
|
+
|
826
|
+
def set(key, value)
|
827
|
+
# Your storage logic
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
config.use_session_store MySessionStore
|
832
|
+
```
|
145
833
|
|
146
834
|
## Development
|
147
835
|
|
148
|
-
|
836
|
+
### Running Tests
|
149
837
|
|
150
|
-
|
838
|
+
FlowChat uses Minitest for testing:
|
151
839
|
|
152
|
-
|
840
|
+
```bash
|
841
|
+
# Run all tests
|
842
|
+
bundle exec rake test
|
153
843
|
|
154
|
-
|
844
|
+
# Run specific test file
|
845
|
+
bundle exec rake test TEST=test/unit/flow_test.rb
|
846
|
+
|
847
|
+
# Run specific test
|
848
|
+
bundle exec rake test TESTOPTS="--name=test_flow_initialization"
|
849
|
+
```
|
850
|
+
|
851
|
+
### Contributing
|
852
|
+
|
853
|
+
1. Fork the repository
|
854
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
855
|
+
3. Add tests for your changes
|
856
|
+
4. Ensure all tests pass (`bundle exec rake test`)
|
857
|
+
5. Commit your changes (`git commit -am 'Add amazing feature'`)
|
858
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
859
|
+
7. Open a Pull Request
|
860
|
+
|
861
|
+
## Roadmap
|
862
|
+
|
863
|
+
- ๐ฌ **Telegram Bot Support** - Native Telegram bot integration
|
864
|
+
- ๐ **Sub-flows** - Reusable conversation components and flow composition
|
865
|
+
- ๐ **Analytics Integration** - Built-in conversation analytics and user journey tracking
|
866
|
+
- ๐ **Multi-language Support** - Internationalization and localization features
|
867
|
+
- โก **Performance Optimizations** - Improved middleware performance and caching
|
868
|
+
- ๐ฏ **Advanced Validation** - More validation helpers and custom validators
|
869
|
+
- ๐ **Enhanced Security** - Rate limiting, input sanitization, and fraud detection
|
155
870
|
|
156
871
|
## License
|
157
872
|
|
158
|
-
|
873
|
+
FlowChat is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
874
|
+
|
875
|
+
## Support
|
876
|
+
|
877
|
+
- ๐ **Documentation**: [GitHub Repository](https://github.com/radioactive-labs/flow_chat)
|
878
|
+
- ๐ **Bug Reports**: [GitHub Issues](https://github.com/radioactive-labs/flow_chat/issues)
|
879
|
+
- ๐ฌ **Community**: Join our discussions for help and feature requests
|