flow_chat 0.6.1 → 0.8.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +85 -1229
  5. data/docs/configuration.md +360 -0
  6. data/docs/flows.md +320 -0
  7. data/docs/images/simulator.png +0 -0
  8. data/docs/instrumentation.md +216 -0
  9. data/docs/media.md +153 -0
  10. data/docs/sessions.md +433 -0
  11. data/docs/testing.md +475 -0
  12. data/docs/ussd-setup.md +322 -0
  13. data/docs/whatsapp-setup.md +162 -0
  14. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  15. data/examples/simulator_controller.rb +13 -22
  16. data/examples/ussd_controller.rb +41 -41
  17. data/examples/whatsapp_controller.rb +32 -125
  18. data/examples/whatsapp_media_examples.rb +68 -336
  19. data/examples/whatsapp_message_job.rb +5 -3
  20. data/flow_chat.gemspec +6 -2
  21. data/lib/flow_chat/base_processor.rb +79 -2
  22. data/lib/flow_chat/config.rb +31 -5
  23. data/lib/flow_chat/context.rb +13 -1
  24. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  25. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  26. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  27. data/lib/flow_chat/instrumentation.rb +70 -0
  28. data/lib/flow_chat/prompt.rb +20 -20
  29. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  30. data/lib/flow_chat/session/middleware.rb +130 -12
  31. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  32. data/lib/flow_chat/simulator/controller.rb +8 -8
  33. data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
  34. data/lib/flow_chat/ussd/gateway/nalo.rb +31 -0
  35. data/lib/flow_chat/ussd/gateway/nsano.rb +36 -2
  36. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  37. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  38. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  39. data/lib/flow_chat/ussd/processor.rb +16 -4
  40. data/lib/flow_chat/ussd/renderer.rb +1 -1
  41. data/lib/flow_chat/version.rb +1 -1
  42. data/lib/flow_chat/whatsapp/client.rb +99 -12
  43. data/lib/flow_chat/whatsapp/configuration.rb +35 -4
  44. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +121 -34
  45. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  46. data/lib/flow_chat/whatsapp/processor.rb +7 -1
  47. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  48. data/lib/flow_chat.rb +23 -0
  49. metadata +23 -12
  50. data/.travis.yml +0 -6
  51. data/app/controllers/demo_controller.rb +0 -101
  52. data/app/flow_chat/demo_restaurant_flow.rb +0 -889
  53. data/config/routes_demo.rb +0 -59
  54. data/examples/initializer.rb +0 -86
  55. data/examples/media_prompts_examples.rb +0 -27
  56. data/images/ussd_simulator.png +0 -0
  57. data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
@@ -0,0 +1,360 @@
1
+ # Configuration Reference
2
+
3
+ This document covers all FlowChat configuration options.
4
+
5
+ ## Framework Configuration
6
+
7
+ ```ruby
8
+ # config/initializers/flowchat.rb
9
+
10
+ # Core configuration
11
+ FlowChat::Config.logger = Rails.logger
12
+ FlowChat::Config.cache = Rails.cache
13
+ FlowChat::Config.simulator_secret = "your_secure_secret_here"
14
+
15
+ # Validation error display behavior
16
+ FlowChat::Config.combine_validation_error_with_message = true # default
17
+
18
+ # Setup instrumentation (optional)
19
+ FlowChat.setup_instrumentation!
20
+ ```
21
+
22
+ ## Session Configuration
23
+
24
+ ```ruby
25
+ # Session boundaries control how session IDs are constructed
26
+ FlowChat::Config.session.boundaries = [:flow, :platform] # default
27
+ FlowChat::Config.session.hash_phone_numbers = true # hash phone numbers for privacy
28
+ FlowChat::Config.session.identifier = nil # let platforms choose (default)
29
+
30
+ # Available boundary options:
31
+ # :flow - separate sessions per flow class
32
+ # :platform - separate sessions per platform (ussd, whatsapp)
33
+ # :gateway - separate sessions per gateway
34
+ # [] - global sessions (no boundaries)
35
+
36
+ # Available identifier options:
37
+ # nil - platform chooses default (:request_id for USSD, :msisdn for WhatsApp)
38
+ # :msisdn - use phone number (durable sessions)
39
+ # :request_id - use request ID (ephemeral sessions)
40
+ ```
41
+
42
+ ## USSD Configuration
43
+
44
+ ```ruby
45
+ # USSD pagination settings
46
+ FlowChat::Config.ussd.pagination_page_size = 140 # characters per page
47
+ FlowChat::Config.ussd.pagination_next_option = "#" # option to go to next page
48
+ FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
49
+ FlowChat::Config.ussd.pagination_back_option = "0" # option to go back
50
+ FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
51
+ ```
52
+
53
+ ## WhatsApp Configuration
54
+
55
+ ```ruby
56
+ # Message handling modes
57
+ FlowChat::Config.whatsapp.message_handling_mode = :inline # :inline, :background, :simulator
58
+ FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
59
+ ```
60
+
61
+ ### WhatsApp Credential Configuration
62
+
63
+ #### Option 1: Rails Credentials
64
+
65
+ ```bash
66
+ rails credentials:edit
67
+ ```
68
+
69
+ ```yaml
70
+ whatsapp:
71
+ access_token: "your_access_token"
72
+ phone_number_id: "your_phone_number_id"
73
+ verify_token: "your_verify_token"
74
+ app_id: "your_app_id"
75
+ app_secret: "your_app_secret"
76
+ business_account_id: "your_business_account_id"
77
+ skip_signature_validation: false
78
+ ```
79
+
80
+ #### Option 2: Environment Variables
81
+
82
+ ```bash
83
+ export WHATSAPP_ACCESS_TOKEN="your_access_token"
84
+ export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
85
+ export WHATSAPP_VERIFY_TOKEN="your_verify_token"
86
+ export WHATSAPP_APP_ID="your_app_id"
87
+ export WHATSAPP_APP_SECRET="your_app_secret"
88
+ export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
89
+ export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
90
+ ```
91
+
92
+ #### Option 3: Programmatic Configuration
93
+
94
+ ```ruby
95
+ config = FlowChat::Whatsapp::Configuration.new(:my_config) # Named configuration
96
+ config.access_token = "your_access_token"
97
+ config.phone_number_id = "your_phone_number_id"
98
+ config.verify_token = "your_verify_token"
99
+ config.app_id = "your_app_id"
100
+ config.app_secret = "your_app_secret"
101
+ config.business_account_id = "your_business_account_id"
102
+ config.skip_signature_validation = false
103
+ # Configuration is automatically registered as :my_config
104
+ ```
105
+
106
+ **⚠️ Important for Background Jobs:** When using background mode with programmatic configurations, you must register them in an initializer:
107
+
108
+ ```ruby
109
+ # config/initializers/whatsapp_configs.rb
110
+ # Register configurations so background jobs can access them
111
+ production_config = FlowChat::Whatsapp::Configuration.new(:production)
112
+ production_config.access_token = ENV['PROD_WHATSAPP_TOKEN']
113
+ # ... other settings
114
+
115
+ staging_config = FlowChat::Whatsapp::Configuration.new(:staging)
116
+ staging_config.access_token = ENV['STAGING_WHATSAPP_TOKEN']
117
+ # ... other settings
118
+ ```
119
+
120
+ Then use named configurations in controllers:
121
+
122
+ ```ruby
123
+ # Use registered configuration
124
+ config = FlowChat::Whatsapp::Configuration.get(:production)
125
+ processor = FlowChat::Whatsapp::Processor.new(self) do |config|
126
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, config
127
+ end
128
+ ```
129
+
130
+ ## Security Configuration
131
+
132
+ ### WhatsApp Security
133
+
134
+ ```ruby
135
+ # Production security (recommended)
136
+ config.app_secret = "your_whatsapp_app_secret"
137
+ config.skip_signature_validation = false # default
138
+
139
+ # Development mode (disable validation)
140
+ config.app_secret = nil
141
+ config.skip_signature_validation = true
142
+ ```
143
+
144
+ ### Simulator Security
145
+
146
+ ```ruby
147
+ # Use Rails secret for uniqueness
148
+ FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
149
+
150
+ # Or use dedicated secret
151
+ FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
152
+ ```
153
+
154
+ ## Environment-Specific Configuration
155
+
156
+ ```ruby
157
+ # config/initializers/flowchat.rb
158
+ case Rails.env
159
+ when 'development'
160
+ FlowChat::Config.whatsapp.message_handling_mode = :simulator
161
+ FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
162
+
163
+ when 'test'
164
+ FlowChat::Config.whatsapp.message_handling_mode = :simulator
165
+ FlowChat::Config.simulator_secret = "test_secret"
166
+
167
+ when 'staging'
168
+ FlowChat::Config.whatsapp.message_handling_mode = :inline
169
+ FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
170
+
171
+ when 'production'
172
+ FlowChat::Config.whatsapp.message_handling_mode = :background
173
+ FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
174
+ FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
175
+ end
176
+ ```
177
+
178
+ ## Processor Configuration
179
+
180
+ ### USSD Processor
181
+
182
+ ```ruby
183
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
184
+ # Gateway (required)
185
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
186
+
187
+ # Session storage (required)
188
+ config.use_session_store FlowChat::Session::CacheSessionStore
189
+
190
+ # Optional middleware
191
+ config.use_middleware MyCustomMiddleware
192
+
193
+ # Configure session boundaries
194
+ config.use_session_config(
195
+ boundaries: [:flow, :platform], # which boundaries to enforce
196
+ hash_phone_numbers: true, # hash phone numbers for privacy
197
+ identifier: :msisdn # use MSISDN for durable sessions (optional)
198
+ )
199
+
200
+ # Shorthand for durable sessions (identifier: :msisdn)
201
+ config.use_durable_sessions
202
+ end
203
+ ```
204
+
205
+ ### WhatsApp Processor
206
+
207
+ ```ruby
208
+ processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
209
+ # Gateway (required)
210
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
211
+
212
+ # Session storage (required)
213
+ config.use_session_store FlowChat::Session::CacheSessionStore
214
+
215
+ # Optional custom configuration
216
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_whatsapp_config
217
+ end
218
+ ```
219
+
220
+ ## Session Store Options
221
+
222
+ ### Cache Session Store
223
+
224
+ ```ruby
225
+ config.use_session_store FlowChat::Session::CacheSessionStore
226
+ ```
227
+
228
+ Uses Rails cache backend with automatic TTL management. This is the primary session store available in FlowChat.
229
+
230
+ ## Middleware Configuration
231
+
232
+ ### Built-in Middleware
233
+
234
+ ```ruby
235
+ # Pagination (USSD only, automatic)
236
+ FlowChat::Ussd::Middleware::Pagination
237
+
238
+ # Session management (automatic)
239
+ FlowChat::Session::Middleware
240
+
241
+ # Gateway communication (automatic)
242
+ FlowChat::Ussd::Gateway::Nalo
243
+ FlowChat::Whatsapp::Gateway::CloudApi
244
+ ```
245
+
246
+ ### Custom Middleware
247
+
248
+ ```ruby
249
+ class LoggingMiddleware
250
+ def initialize(app)
251
+ @app = app
252
+ end
253
+
254
+ def call(context)
255
+ Rails.logger.info "Processing request: #{context.input}"
256
+ result = @app.call(context)
257
+ Rails.logger.info "Response: #{result[1]}"
258
+ result
259
+ end
260
+ end
261
+
262
+ # Use custom middleware
263
+ config.use_middleware LoggingMiddleware
264
+ ```
265
+
266
+ ## Validation Configuration
267
+
268
+ ### Error Display Options
269
+
270
+ ```ruby
271
+ # Combine validation error with original message (default)
272
+ FlowChat::Config.combine_validation_error_with_message = true
273
+ # User sees: "Invalid email format\n\nEnter your email:"
274
+
275
+ # Show only validation error
276
+ FlowChat::Config.combine_validation_error_with_message = false
277
+ # User sees: "Invalid email format"
278
+ ```
279
+
280
+ ## Background Job Configuration
281
+
282
+ ### Job Class Setup
283
+
284
+ ```ruby
285
+ # app/jobs/whatsapp_message_job.rb
286
+ class WhatsappMessageJob < ApplicationJob
287
+ include FlowChat::Whatsapp::SendJobSupport
288
+
289
+ def perform(send_data)
290
+ perform_whatsapp_send(send_data)
291
+ end
292
+ end
293
+ ```
294
+
295
+ **Configuration Resolution:** The job automatically resolves configurations using:
296
+ 1. Named configuration from `send_data[:configuration_name]` if present
297
+ 2. Default configuration from credentials/environment variables
298
+
299
+ For custom resolution logic, override the configuration resolution:
300
+
301
+ ```ruby
302
+ class CustomWhatsappMessageJob < ApplicationJob
303
+ include FlowChat::Whatsapp::SendJobSupport
304
+
305
+ def perform(send_data)
306
+ perform_whatsapp_send(send_data)
307
+ end
308
+
309
+ private
310
+
311
+ def resolve_whatsapp_configuration(send_data)
312
+ # Custom logic to resolve configuration
313
+ tenant_id = ...
314
+ FlowChat::Whatsapp::Configuration.get("tenant_#{tenant_id}")
315
+ end
316
+ end
317
+ ```
318
+
319
+ ### Queue Configuration
320
+
321
+ ```ruby
322
+ # config/application.rb
323
+ config.active_job.queue_adapter = :sidekiq
324
+
325
+ # config/initializers/flowchat.rb
326
+ FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
327
+ ```
328
+
329
+ ## Instrumentation Configuration
330
+
331
+ ### Basic Setup
332
+
333
+ ```ruby
334
+ # Enable instrumentation
335
+ FlowChat.setup_instrumentation!
336
+ ```
337
+
338
+ ### Custom Event Subscribers
339
+
340
+ ```ruby
341
+ # Subscribe to specific events
342
+ ActiveSupport::Notifications.subscribe("flow.execution.end.flow_chat") do |event|
343
+ # Custom handling
344
+ ExternalMonitoring.track_flow_execution(
345
+ event.payload[:flow_name],
346
+ event.duration
347
+ )
348
+ end
349
+
350
+ # Subscribe to all FlowChat events
351
+ ActiveSupport::Notifications.subscribe(/\.flow_chat$/) do |name, start, finish, id, payload|
352
+ CustomLogger.log_event(name, payload.merge(duration: finish - start))
353
+ end
354
+ ```
355
+
356
+ ## Configuration Validation
357
+
358
+ FlowChat validates configuration at runtime and provides helpful error messages:
359
+
360
+ FlowChat validates configuration at runtime and provides helpful error messages for missing or invalid configurations.
data/docs/flows.md ADDED
@@ -0,0 +1,320 @@
1
+ # Flow Development Guide
2
+
3
+ This guide covers advanced flow patterns, validation techniques, and best practices for building sophisticated conversational workflows.
4
+
5
+ ## Flow Architecture
6
+
7
+ ### Flow Lifecycle
8
+
9
+ Every flow method must result in user interaction:
10
+
11
+ ```ruby
12
+ class ExampleFlow < FlowChat::Flow
13
+ def main_page
14
+ # ✅ Always end with user interaction
15
+ choice = app.screen(:choice) { |p| p.select "Choose:", ["A", "B"] }
16
+
17
+ case choice
18
+ when "A"
19
+ handle_option_a
20
+ app.say "Option A completed!" # Required interaction
21
+ when "B"
22
+ handle_option_b
23
+ app.say "Option B completed!" # Required interaction
24
+ end
25
+ end
26
+ end
27
+ ```
28
+
29
+ ### Session Management
30
+
31
+ FlowChat automatically persists screen results:
32
+
33
+ ```ruby
34
+ class RegistrationFlow < FlowChat::Flow
35
+ def main_page
36
+ # These values persist across requests
37
+ name = app.screen(:name) { |p| p.ask "Name?" }
38
+ email = app.screen(:email) { |p| p.ask "Email?" }
39
+
40
+ # Show summary using cached values
41
+ confirmed = app.screen(:confirm) do |prompt|
42
+ prompt.yes? "Create account for #{name} (#{email})?"
43
+ end
44
+
45
+ if confirmed
46
+ create_user(name: name, email: email)
47
+ app.say "Account created!"
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ ## Input Validation Patterns
54
+
55
+ ### Basic Validation
56
+
57
+ ```ruby
58
+ age = app.screen(:age) do |prompt|
59
+ prompt.ask "Enter your age:",
60
+ validate: ->(input) {
61
+ return "Age must be a number" unless input.match?(/^\d+$/)
62
+ return "Must be 18 or older" unless input.to_i >= 18
63
+ nil # Return nil for valid input
64
+ },
65
+ transform: ->(input) { input.to_i }
66
+ end
67
+ ```
68
+
69
+ ### Complex Validation
70
+
71
+ ```ruby
72
+ phone = app.screen(:phone) do |prompt|
73
+ prompt.ask "Enter phone number:",
74
+ validate: ->(input) {
75
+ clean = input.gsub(/[\s\-\(\)]/, '')
76
+ return "Invalid format" unless clean.match?(/^\+?[\d]{10,15}$/)
77
+ return "Must start with country code" unless clean.start_with?('+')
78
+ nil
79
+ },
80
+ transform: ->(input) { input.gsub(/[\s\-\(\)]/, '') }
81
+ end
82
+ ```
83
+
84
+ ### Conditional Validation
85
+
86
+ ```ruby
87
+ class PaymentFlow < FlowChat::Flow
88
+ def collect_payment_method
89
+ method = app.screen(:method) do |prompt|
90
+ prompt.select "Payment method:", ["card", "mobile_money"]
91
+ end
92
+
93
+ if method == "card"
94
+ collect_card_details
95
+ else
96
+ collect_mobile_money_details
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def collect_card_details
103
+ card = app.screen(:card) do |prompt|
104
+ prompt.ask "Card number (16 digits):",
105
+ validate: ->(input) {
106
+ clean = input.gsub(/\s/, '')
107
+ return "Must be 16 digits" unless clean.length == 16
108
+ return "Invalid card number" unless luhn_valid?(clean)
109
+ nil
110
+ }
111
+ end
112
+
113
+ app.say "Card ending in #{card[-4..-1]} saved."
114
+ end
115
+ end
116
+ ```
117
+
118
+ ## Menu Patterns
119
+
120
+ ### Dynamic Menus
121
+
122
+ ```ruby
123
+ def show_products
124
+ products = fetch_available_products
125
+
126
+ choice = app.screen(:product) do |prompt|
127
+ prompt.select "Choose product:", products.map(&:name)
128
+ end
129
+
130
+ selected_product = products.find { |p| p.name == choice }
131
+ show_product_details(selected_product)
132
+ end
133
+ ```
134
+
135
+ ### Nested Menus
136
+
137
+ ```ruby
138
+ def main_menu
139
+ choice = app.screen(:main) do |prompt|
140
+ prompt.select "Main Menu:", {
141
+ "products" => "View Products",
142
+ "orders" => "My Orders",
143
+ "support" => "Customer Support"
144
+ }
145
+ end
146
+
147
+ case choice
148
+ when "products"
149
+ products_menu
150
+ when "orders"
151
+ orders_menu
152
+ when "support"
153
+ support_menu
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## Advanced Patterns
159
+
160
+ ### Multi-Step Forms
161
+
162
+ ```ruby
163
+ class CompleteProfileFlow < FlowChat::Flow
164
+ def main_page
165
+ collect_basic_info
166
+ collect_preferences
167
+ confirm_and_save
168
+ end
169
+
170
+ private
171
+
172
+ def collect_basic_info
173
+ app.screen(:name) { |p| p.ask "Full name:" }
174
+ app.screen(:email) { |p| p.ask "Email:" }
175
+ app.screen(:phone) { |p| p.ask "Phone:" }
176
+ end
177
+
178
+ def collect_preferences
179
+ app.screen(:language) { |p| p.select "Language:", ["English", "French"] }
180
+ app.screen(:notifications) { |p| p.yes? "Enable notifications?" }
181
+ end
182
+
183
+ def confirm_and_save
184
+ summary = build_summary
185
+ confirmed = app.screen(:confirm) { |p| p.yes? "Save profile?\n\n#{summary}" }
186
+
187
+ if confirmed
188
+ save_profile
189
+ app.say "Profile saved successfully!"
190
+ else
191
+ app.say "Profile not saved."
192
+ end
193
+ end
194
+ end
195
+ ```
196
+
197
+ ### Error Recovery
198
+
199
+ ```ruby
200
+ def process_payment
201
+ begin
202
+ amount = app.screen(:amount) do |prompt|
203
+ prompt.ask "Amount to pay:",
204
+ validate: ->(input) {
205
+ return "Invalid amount" unless input.match?(/^\d+(\.\d{2})?$/)
206
+ return "Minimum $1.00" unless input.to_f >= 1.0
207
+ nil
208
+ },
209
+ transform: ->(input) { input.to_f }
210
+ end
211
+
212
+ process_transaction(amount)
213
+ app.say "Payment of $#{amount} processed successfully!"
214
+
215
+ rescue PaymentError => e
216
+ app.say "Payment failed: #{e.message}. Please try again."
217
+ process_payment # Retry
218
+ end
219
+ end
220
+ ```
221
+
222
+ ## Cross-Platform Considerations
223
+
224
+ ### Platform Detection
225
+
226
+ ```ruby
227
+ def show_help
228
+ if app.context["request.gateway"] == :whatsapp_cloud_api
229
+ # WhatsApp users get rich media
230
+ app.say "Here's how to use our service:",
231
+ media: { type: :image, url: "https://example.com/help.jpg" }
232
+ else
233
+ # USSD users get text with link
234
+ app.say "Help guide: https://example.com/help"
235
+ end
236
+ end
237
+ ```
238
+
239
+ ### Progressive Enhancement
240
+
241
+ ```ruby
242
+ def collect_feedback
243
+ rating = app.screen(:rating) do |prompt|
244
+ if whatsapp?
245
+ # Rich interactive buttons for WhatsApp
246
+ prompt.select "Rate our service:", {
247
+ "5" => "⭐⭐⭐⭐⭐ Excellent",
248
+ "4" => "⭐⭐⭐⭐ Good",
249
+ "3" => "⭐⭐⭐ Average",
250
+ "2" => "⭐⭐ Poor",
251
+ "1" => "⭐ Very Poor"
252
+ }
253
+ else
254
+ # Simple numbered list for USSD
255
+ prompt.select "Rate our service (1-5):", ["1", "2", "3", "4", "5"]
256
+ end
257
+ end
258
+
259
+ app.say "Thank you for rating us #{rating} stars!"
260
+ end
261
+
262
+ private
263
+
264
+ def whatsapp?
265
+ app.context["request.gateway"] == :whatsapp_cloud_api
266
+ end
267
+ ```
268
+
269
+ ## Best Practices
270
+
271
+ ### Keep Methods Focused
272
+
273
+ ```ruby
274
+ # ✅ Good: Single responsibility
275
+ def collect_contact_info
276
+ name = app.screen(:name) { |p| p.ask "Name:" }
277
+ email = app.screen(:email) { |p| p.ask "Email:" }
278
+ { name: name, email: email }
279
+ end
280
+
281
+ # ❌ Avoid: Too much in one method
282
+ def handle_everything
283
+ # 50+ lines of mixed logic
284
+ end
285
+ ```
286
+
287
+ ### Use Meaningful Screen Names
288
+
289
+ ```ruby
290
+ # ✅ Good: Descriptive names
291
+ app.screen(:billing_address) { |p| p.ask "Billing address:" }
292
+ app.screen(:confirm_payment) { |p| p.yes? "Confirm $#{amount}?" }
293
+
294
+ # ❌ Avoid: Generic names
295
+ app.screen(:input1) { |p| p.ask "Address:" }
296
+ app.screen(:confirm) { |p| p.yes? "OK?" }
297
+ ```
298
+
299
+ ### Handle Edge Cases
300
+
301
+ ```ruby
302
+ def show_order_history
303
+ orders = fetch_user_orders
304
+
305
+ if orders.empty?
306
+ app.say "You have no previous orders."
307
+ return
308
+ end
309
+
310
+ choice = app.screen(:order) do |prompt|
311
+ prompt.select "Select order:", orders.map(&:display_name)
312
+ end
313
+
314
+ show_order_details(orders.find { |o| o.display_name == choice })
315
+ end
316
+ ```
317
+
318
+ ## Testing Flows
319
+
320
+ See [Testing Guide](testing.md) for comprehensive testing strategies.
Binary file