flow_chat 0.8.2 → 0.10.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/pages.yml +43 -0
  5. data/.github/workflows/release.yml +56 -0
  6. data/.standard.yml +4 -0
  7. data/CHANGELOG.md +48 -0
  8. data/CLAUDE.md +327 -0
  9. data/CONTRIBUTING.md +134 -0
  10. data/Gemfile +1 -0
  11. data/README.md +189 -133
  12. data/Rakefile +17 -2
  13. data/SECURITY.md +42 -349
  14. data/docs/architecture.md +83 -0
  15. data/docs/async-background-processing.md +64 -0
  16. data/docs/configuration.md +110 -287
  17. data/docs/factory-pattern.md +58 -0
  18. data/docs/gateway-context-variables.md +168 -0
  19. data/docs/gateway-development.md +159 -0
  20. data/docs/getting-started.md +90 -0
  21. data/docs/instrumentation.md +95 -175
  22. data/docs/platforms/instagram.md +278 -0
  23. data/docs/platforms/messenger.md +205 -0
  24. data/docs/platforms/telegram.md +109 -0
  25. data/docs/platforms/ussd.md +78 -0
  26. data/docs/platforms/whatsapp.md +147 -0
  27. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  28. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  29. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  30. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  31. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  32. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  33. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  34. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  35. data/docs/testing.md +33 -426
  36. data/examples/custom_session_id_example.rb +119 -0
  37. data/examples/http_controller.rb +22 -20
  38. data/examples/intercom_configuration_example.rb +113 -0
  39. data/examples/intercom_controller.rb +182 -0
  40. data/examples/multi_tenant_whatsapp_controller.rb +63 -168
  41. data/examples/simulator_controller.rb +0 -1
  42. data/examples/ussd_controller.rb +88 -160
  43. data/examples/whatsapp_controller.rb +18 -17
  44. data/examples/whatsapp_media_examples.rb +27 -79
  45. data/flow_chat.gemspec +4 -0
  46. data/lib/flow_chat/app.rb +211 -0
  47. data/lib/flow_chat/async_job.rb +176 -0
  48. data/lib/flow_chat/choice_titles.rb +95 -0
  49. data/lib/flow_chat/config.rb +126 -23
  50. data/lib/flow_chat/delivery_error.rb +9 -0
  51. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  52. data/lib/flow_chat/factory.rb +94 -0
  53. data/lib/flow_chat/gateway_async_support.rb +106 -0
  54. data/lib/flow_chat/generic_async_job.rb +30 -0
  55. data/lib/flow_chat/http/configuration_error.rb +9 -0
  56. data/lib/flow_chat/http/gateway/simple.rb +104 -36
  57. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  58. data/lib/flow_chat/http/renderer.rb +3 -3
  59. data/lib/flow_chat/input.rb +86 -0
  60. data/lib/flow_chat/instagram/client.rb +32 -0
  61. data/lib/flow_chat/instagram/configuration.rb +147 -0
  62. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  63. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  64. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  65. data/lib/flow_chat/instagram/renderer.rb +23 -0
  66. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  67. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  68. data/lib/flow_chat/instrumentation.rb +182 -0
  69. data/lib/flow_chat/intercom/client.rb +161 -0
  70. data/lib/flow_chat/intercom/configuration.rb +102 -0
  71. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  72. data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
  73. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  74. data/lib/flow_chat/intercom/renderer.rb +123 -0
  75. data/lib/flow_chat/media.rb +121 -0
  76. data/lib/flow_chat/messenger/client.rb +264 -0
  77. data/lib/flow_chat/messenger/configuration.rb +103 -0
  78. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  79. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  80. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  81. data/lib/flow_chat/messenger/renderer.rb +150 -0
  82. data/lib/flow_chat/meta/challenge.rb +24 -0
  83. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  84. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  85. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  86. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  87. data/lib/flow_chat/meta/signature.rb +30 -0
  88. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  89. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  90. data/lib/flow_chat/named_configuration.rb +65 -0
  91. data/lib/flow_chat/phone_number_util.rb +37 -35
  92. data/lib/flow_chat/processor.rb +188 -0
  93. data/lib/flow_chat/prompt.rb +13 -16
  94. data/lib/flow_chat/renderers/markdown_support.rb +167 -0
  95. data/lib/flow_chat/security.rb +76 -0
  96. data/lib/flow_chat/session/middleware.rb +36 -11
  97. data/lib/flow_chat/simulator/controller.rb +31 -15
  98. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  99. data/lib/flow_chat/telegram/client.rb +283 -0
  100. data/lib/flow_chat/telegram/configuration.rb +78 -0
  101. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  102. data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
  103. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
  104. data/lib/flow_chat/telegram/renderer.rb +133 -0
  105. data/lib/flow_chat/telegram.rb +7 -0
  106. data/lib/flow_chat/text_truncator.rb +75 -0
  107. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  108. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  109. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  110. data/lib/flow_chat/ussd/renderer.rb +1 -1
  111. data/lib/flow_chat/version.rb +1 -1
  112. data/lib/flow_chat/whatsapp/client.rb +158 -20
  113. data/lib/flow_chat/whatsapp/configuration.rb +13 -52
  114. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  115. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
  116. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
  117. data/lib/flow_chat/whatsapp/renderer.rb +259 -64
  118. data/lib/flow_chat.rb +1 -1
  119. data/lib/tasks/release.rake +165 -0
  120. data/site/.nojekyll +0 -0
  121. data/site/.og-card.html +89 -0
  122. data/site/favicon.svg +6 -0
  123. data/site/index.html +209 -0
  124. data/site/og.png +0 -0
  125. metadata +132 -25
  126. data/docs/flows.md +0 -320
  127. data/docs/http-gateway-protocol.md +0 -432
  128. data/docs/images/simulator.png +0 -0
  129. data/docs/media.md +0 -153
  130. data/docs/sessions.md +0 -433
  131. data/docs/ussd-setup.md +0 -322
  132. data/docs/whatsapp-setup.md +0 -162
  133. data/examples/whatsapp_message_job.rb +0 -113
  134. data/lib/flow_chat/base_app.rb +0 -86
  135. data/lib/flow_chat/base_processor.rb +0 -146
  136. data/lib/flow_chat/http/app.rb +0 -6
  137. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  138. data/lib/flow_chat/http/processor.rb +0 -33
  139. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  140. data/lib/flow_chat/ussd/app.rb +0 -6
  141. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  142. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  143. data/lib/flow_chat/ussd/processor.rb +0 -39
  144. data/lib/flow_chat/whatsapp/app.rb +0 -29
  145. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  146. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  147. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
data/docs/testing.md CHANGED
@@ -1,30 +1,23 @@
1
- # Testing Guide
1
+ # Testing
2
2
 
3
- FlowChat provides comprehensive testing capabilities for both USSD and WhatsApp flows. This guide covers everything from unit testing individual flows to using the powerful built-in simulator for interactive testing.
3
+ FlowChat gives you two ways to exercise a flow without a live gateway: a web simulator for manual, interactive testing during development, and the HTTP gateway for automated request tests.
4
4
 
5
- ## Testing Approaches
5
+ ## The simulator
6
6
 
7
- FlowChat supports multiple testing strategies depending on your needs:
7
+ The simulator is a web page that drives your flows the way a real platform would, so you can step through a conversation from the browser. It works the same regardless of which gateway a flow targets.
8
8
 
9
- | Approach | Best For | Setup Complexity | Real API Calls |
10
- |----------|----------|------------------|----------------|
11
- | **Unit Testing** | Individual flow logic | Low | No |
12
- | **Simulator Mode** | Integration testing, development | Medium | No |
13
- | **Skip Validation** | Staging environments | Medium | Yes |
14
- | **Full Integration** | Production-like testing | High | Yes |
9
+ Enable it in two steps.
15
10
 
16
- ## Quick Start: Interactive Simulator
17
-
18
- The fastest way to test your flows is with the built-in web simulator:
19
-
20
- ### 1. Configure Simulator
11
+ Set the simulator secret during boot:
21
12
 
22
13
  ```ruby
23
- # config/initializers/flowchat.rb
24
- FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
14
+ # config/initializers/flow_chat.rb
15
+ FlowChat::Config.simulator_secret = Rails.application.credentials.flow_chat_simulator_secret
25
16
  ```
26
17
 
27
- ### 2. Create Simulator Controller
18
+ The secret gates access. The simulator controller signs a cookie with it, and the gateways only enter simulator mode when that cookie is valid, so the simulator stays off in any environment where the secret is unset.
19
+
20
+ Mount a controller that includes the simulator module and lists the endpoints to test:
28
21
 
29
22
  ```ruby
30
23
  # app/controllers/simulator_controller.rb
@@ -39,437 +32,51 @@ class SimulatorController < ApplicationController
39
32
 
40
33
  def configurations
41
34
  {
42
- ussd: {
43
- name: "USSD Integration",
44
- icon: "📱",
35
+ ussd_main: {
36
+ name: "USSD",
45
37
  processor_type: "ussd",
46
38
  gateway: "nalo",
47
- endpoint: "/ussd",
48
- color: "#007bff"
39
+ endpoint: "/ussd"
49
40
  },
50
- whatsapp: {
51
- name: "WhatsApp Integration",
52
- icon: "💬",
53
- processor_type: "whatsapp",
41
+ whatsapp_main: {
42
+ name: "WhatsApp",
43
+ processor_type: "whatsapp",
54
44
  gateway: "cloud_api",
55
- endpoint: "/whatsapp/webhook",
56
- color: "#25D366"
45
+ endpoint: "/whatsapp/webhook"
57
46
  }
58
47
  }
59
48
  end
60
-
61
- def default_config_key
62
- :whatsapp
63
- end
64
-
65
- def default_phone_number
66
- "+1234567890"
67
- end
68
-
69
- def default_contact_name
70
- "Test User"
71
- end
72
49
  end
73
50
  ```
74
51
 
75
- ### 3. Add Route
76
-
77
52
  ```ruby
78
53
  # config/routes.rb
79
- Rails.application.routes.draw do
80
- get '/simulator' => 'simulator#index'
81
- # ... your other routes
82
- end
83
- ```
84
-
85
- ### 4. Enable Simulator in Controllers
86
-
87
- ```ruby
88
- # app/controllers/whatsapp_controller.rb
89
- class WhatsappController < ApplicationController
90
- skip_forgery_protection
91
-
92
- def webhook
93
- enable_simulator = Rails.env.development? # enabled in development by default
94
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator:) do |config|
95
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
96
- config.use_session_store FlowChat::Session::CacheSessionStore
97
- end
98
-
99
- processor.run WelcomeFlow, :main_page
100
- end
101
- end
102
- ```
103
-
104
- ### 5. Test Your Flows
105
-
106
- Visit [http://localhost:3000/simulator](http://localhost:3000/simulator) and start testing!
107
-
108
- **Simulator Features:**
109
- - 📱 **Visual Interface** - Phone-like display showing actual conversation
110
- - 🔄 **Platform Switching** - Toggle between USSD and WhatsApp modes
111
- - 📊 **Request Logging** - See HTTP requests and responses in real-time
112
- - 🎯 **Interactive Testing** - Character counting, validation, session management
113
- - 🛠️ **Developer Tools** - Reset sessions, view connection status
114
-
115
- ## Unit Testing
116
-
117
- Test individual flows in isolation:
118
-
119
- ### Basic Flow Testing
120
-
121
- ```ruby
122
- # test/flows/welcome_flow_test.rb
123
- require 'test_helper'
124
-
125
- class WelcomeFlowTest < ActiveSupport::TestCase
126
- def setup
127
- @context = FlowChat::Context.new
128
- @context.session = FlowChat::Session::CacheSessionStore.new
129
- @context.session.init_session("test_session")
130
- end
131
-
132
- test "welcome flow collects name and shows greeting" do
133
- # Simulate user entering name
134
- @context.input = "John Doe"
135
- app = FlowChat::Ussd::App.new(@context)
136
-
137
- # Expect flow to terminate with greeting
138
- error = assert_raises(FlowChat::Interrupt::Terminate) do
139
- flow = WelcomeFlow.new(app)
140
- flow.main_page
141
- end
142
-
143
- assert_includes error.prompt, "Hello, John Doe"
144
- end
145
-
146
- test "flow handles validation errors" do
147
- # Test with empty input
148
- @context.input = ""
149
- app = FlowChat::Ussd::App.new(@context)
150
-
151
- # Should prompt for input again
152
- error = assert_raises(FlowChat::Interrupt::Input) do
153
- flow = RegistrationFlow.new(app)
154
- flow.collect_email
155
- end
156
-
157
- assert_includes error.prompt, "Email is required"
158
- end
159
- end
160
- ```
161
-
162
- ### Testing Complex Flows
163
-
164
- ```ruby
165
- # test/flows/registration_flow_test.rb
166
- class RegistrationFlowTest < ActiveSupport::TestCase
167
- test "complete registration flow" do
168
- context = FlowChat::Context.new
169
- context.session = FlowChat::Session::CacheSessionStore.new
170
- context.session.init_session("test_session")
171
-
172
- # Step 1: Enter email
173
- context.input = "john@example.com"
174
- app = FlowChat::Ussd::App.new(context)
175
-
176
- assert_raises(FlowChat::Interrupt::Input) do
177
- flow = RegistrationFlow.new(app)
178
- flow.main_page
179
- end
180
-
181
- # Verify email was stored
182
- assert_equal "john@example.com", context.session.get(:email)
183
-
184
- # Step 2: Enter age
185
- context.input = "25"
186
-
187
- assert_raises(FlowChat::Interrupt::Input) do
188
- flow = RegistrationFlow.new(app)
189
- flow.main_page # Continue from where we left off
190
- end
191
-
192
- # Step 3: Confirm
193
- context.input = "yes"
194
-
195
- assert_raises(FlowChat::Interrupt::Terminate) do
196
- flow = RegistrationFlow.new(app)
197
- flow.main_page
198
- end
199
- end
200
- end
201
- ```
202
-
203
- ## Integration Testing
204
-
205
- ### Environment Configuration
206
-
207
- Set up different testing modes per environment:
208
-
209
- ```ruby
210
- # config/initializers/flowchat.rb
211
- case Rails.env
212
- when 'development'
213
- # Use simulator for easy testing
214
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
215
- FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
216
-
217
- when 'test'
218
- # Use simulator for automated tests
219
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
220
- FlowChat::Config.simulator_secret = "test_secret_key"
221
-
222
- when 'staging'
223
- # Use inline mode with real WhatsApp API but skip validation for testing
224
- FlowChat::Config.whatsapp.message_handling_mode = :inline
225
- FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
226
-
227
- when 'production'
228
- # Use background jobs with full security
229
- FlowChat::Config.whatsapp.message_handling_mode = :background
230
- FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
231
- # No simulator secret in production
232
- end
233
- ```
234
-
235
- ### Simulator Mode Testing
236
-
237
- Test webhook endpoints using simulator mode:
238
-
239
- ```ruby
240
- # test/controllers/whatsapp_controller_test.rb
241
- class WhatsappControllerTest < ActionDispatch::IntegrationTest
242
- test "processes whatsapp message in simulator mode" do
243
- webhook_payload = {
244
- entry: [{
245
- changes: [{
246
- value: {
247
- messages: [{
248
- from: "1234567890",
249
- text: { body: "Hello" },
250
- type: "text",
251
- id: "msg_123",
252
- timestamp: Time.now.to_i
253
- }]
254
- }
255
- }]
256
- }],
257
- simulator_mode: true # Enable simulator mode
258
- }
259
-
260
- # Generate valid simulator cookie
261
- valid_cookie = generate_simulator_cookie
262
-
263
- post "/whatsapp/webhook",
264
- params: webhook_payload,
265
- cookies: { flowchat_simulator: valid_cookie }
266
-
267
- assert_response :success
268
-
269
- # In simulator mode, response contains message data
270
- response_data = JSON.parse(response.body)
271
- assert response_data.key?("text")
272
- assert_includes response_data["text"], "What's your name?"
273
- end
274
-
275
- test "multi-step flow maintains state" do
276
- valid_cookie = generate_simulator_cookie
277
-
278
- # Step 1: Start conversation
279
- post_simulator_message("start", valid_cookie)
280
- assert_response :success
281
-
282
- # Step 2: Enter name
283
- post_simulator_message("John", valid_cookie)
284
- assert_response :success
285
-
286
- response_data = JSON.parse(response.body)
287
- assert_includes response_data["text"], "Hello John"
288
- end
289
-
290
- private
291
-
292
- def generate_simulator_cookie(secret = "test_secret_key")
293
- timestamp = Time.now.to_i
294
- message = "simulator:#{timestamp}"
295
- signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, message)
296
- "#{timestamp}:#{signature}"
297
- end
298
-
299
- def post_simulator_message(text, cookie)
300
- webhook_payload = {
301
- entry: [{
302
- changes: [{
303
- value: {
304
- messages: [{
305
- from: "1234567890",
306
- text: { body: text },
307
- type: "text",
308
- id: "msg_#{rand(1000)}",
309
- timestamp: Time.now.to_i
310
- }]
311
- }
312
- }]
313
- }],
314
- simulator_mode: true
315
- }
316
-
317
- post "/whatsapp/webhook",
318
- params: webhook_payload,
319
- cookies: { flowchat_simulator: cookie }
320
- end
321
- end
322
- ```
323
-
324
- ### Testing with Disabled Validation
325
-
326
- For staging environments where you want to test real endpoints:
327
-
328
- ```ruby
329
- test "webhook with disabled validation" do
330
- # Create config with validation disabled
331
- config = FlowChat::Whatsapp::Configuration.new(:test_config)
332
- config.access_token = "test_token"
333
- config.phone_number_id = "test_phone_id"
334
- config.verify_token = "test_verify"
335
- config.skip_signature_validation = true # Disable validation for testing
336
-
337
- webhook_payload = {
338
- entry: [{
339
- changes: [{
340
- value: {
341
- messages: [{
342
- from: "1234567890",
343
- text: { body: "Hello" },
344
- type: "text"
345
- }]
346
- }
347
- }]
348
- }]
349
- }
350
-
351
- post "/whatsapp/webhook",
352
- params: webhook_payload.to_json,
353
- headers: { "Content-Type" => "application/json" }
354
-
355
- assert_response :success
356
- end
357
- ```
358
-
359
- ## Advanced Testing Scenarios
360
-
361
- ### Testing Error Handling
362
-
363
- ```ruby
364
- test "handles validation errors gracefully" do
365
- valid_cookie = generate_simulator_cookie
366
-
367
- # Send invalid email
368
- post_simulator_message("invalid-email", valid_cookie)
369
-
370
- response_data = JSON.parse(response.body)
371
- assert_includes response_data["text"], "Invalid email format"
372
-
373
- # Send valid email - should proceed
374
- post_simulator_message("john@example.com", valid_cookie)
375
-
376
- response_data = JSON.parse(response.body)
377
- refute_includes response_data["text"], "Invalid email"
378
- end
54
+ get "/simulator", to: "simulator#index"
379
55
  ```
380
56
 
381
- ### Testing Media Responses
57
+ Each entry in `configurations` points at one of your real webhook endpoints, so the simulator posts to the same controller actions the platform would. Open `/simulator`, pick an endpoint, and send messages.
382
58
 
383
- ```ruby
384
- test "media responses in simulator mode" do
385
- valid_cookie = generate_simulator_cookie
386
-
387
- post_simulator_message("help", valid_cookie)
388
-
389
- response_data = JSON.parse(response.body)
390
-
391
- # Check media is included
392
- assert response_data.key?("media")
393
- assert_equal "image", response_data["media"]["type"]
394
- assert response_data["media"]["url"].present?
395
- end
396
- ```
59
+ ## Automated tests
397
60
 
398
- ### Testing Session Persistence
61
+ For request specs, drive a flow through the HTTP gateway (`FlowChat::Http::Gateway::Simple`), which speaks plain JSON. Point a controller at your flow:
399
62
 
400
63
  ```ruby
401
- test "session data persists across requests" do
402
- valid_cookie = generate_simulator_cookie
403
-
404
- # First request - enter name
405
- post_simulator_message("John", valid_cookie)
406
-
407
- # Second request - session should remember name
408
- post_simulator_message("continue", valid_cookie)
409
-
410
- response_data = JSON.parse(response.body)
411
- assert_includes response_data["text"], "John" # Name should be remembered
412
- end
413
- ```
414
-
415
- ## Performance Testing
416
-
417
- ### Load Testing Background Jobs
418
-
419
- ```ruby
420
- test "handles high message volume with background jobs" do
421
- # Switch to background mode for this test
422
- original_mode = FlowChat::Config.whatsapp.message_handling_mode
423
- FlowChat::Config.whatsapp.message_handling_mode = :background
424
-
425
- messages = 10.times.map do |i|
426
- create_whatsapp_message_payload("user#{i}")
427
- end
428
-
429
- assert_enqueued_jobs 10 do
430
- messages.each do |msg|
431
- post "/whatsapp/webhook", params: msg
64
+ class ChatController < ApplicationController
65
+ def webhook
66
+ processor = FlowChat::Processor.new(self) do |config|
67
+ config.use_gateway FlowChat::Http::Gateway::Simple
68
+ config.use_session_store FlowChat::Session::CacheSessionStore
432
69
  end
70
+ processor.run RegistrationFlow, :main_page
433
71
  end
434
- ensure
435
- FlowChat::Config.whatsapp.message_handling_mode = original_mode
436
72
  end
437
73
  ```
438
74
 
439
- ## Debugging Tests
75
+ The Simple gateway expects `session_id` and `user_id` in the request and returns JSON with the prompt, choices, and any media. Post the next input with the same `session_id` to advance the conversation, and assert on the returned message and choices. Set `FlowChat::Config.cache` to a real store (for example `ActiveSupport::Cache::MemoryStore.new`) in your test setup so sessions persist across the requests in a single test.
440
76
 
441
- ### Enable Debug Logging
77
+ Because one flow behaves the same across platforms, a flow verified through the HTTP gateway behaves the same on USSD, WhatsApp, and Telegram, apart from each platform's rendering.
442
78
 
443
- ```ruby
444
- # config/environments/test.rb
445
- config.log_level = :debug
79
+ ## Related
446
80
 
447
- # In tests
448
- Rails.logger.debug "Current session data: #{context.session.data}"
449
- ```
450
-
451
- ### Inspect Flow State
452
-
453
- ```ruby
454
- test "debug flow execution" do
455
- context = FlowChat::Context.new
456
- context.session = FlowChat::Session::CacheSessionStore.new
457
- context.session.init_session("debug_session")
458
-
459
- # Add debugging
460
- context.input = "test@example.com"
461
- app = FlowChat::Ussd::App.new(context)
462
-
463
- flow = RegistrationFlow.new(app)
464
-
465
- # Inspect state before execution
466
- puts "Session before: #{context.session.data}"
467
-
468
- begin
469
- flow.main_page
470
- rescue FlowChat::Interrupt::Input => e
471
- puts "Flow interrupted with: #{e.prompt}"
472
- puts "Session after: #{context.session.data}"
473
- end
474
- end
475
- ```
81
+ - [Getting started](getting-started.md)
82
+ - [Configuration](configuration.md)
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example: Custom Session ID Configuration with Proc
4
+ #
5
+ # This example demonstrates how to use a custom proc for session ID generation
6
+ # in FlowChat applications. The proc allows complete customization of how
7
+ # session IDs are generated based on context data.
8
+
9
+ require "flow_chat"
10
+
11
+ class CustomSessionIdController < ApplicationController
12
+ def ussd_endpoint
13
+ processor = FlowChat::Processor.new(self) do |config|
14
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
15
+ config.use_session_store FlowChat::Session::CacheSessionStore
16
+
17
+ # Example 1: Custom session ID using block/proc
18
+ config.use_session_config do |context|
19
+ # Create a custom session ID based on your business logic
20
+ user_phone = context["request.msisdn"]
21
+ flow_name = context["flow.name"]
22
+ gateway = context["request.gateway"]
23
+ timestamp = Time.current.strftime("%Y%m%d")
24
+
25
+ # Custom format: flow_gateway_date_hashedphone
26
+ "#{flow_name}_#{gateway}_#{timestamp}_#{hash_phone(user_phone)}"
27
+ end
28
+ end
29
+
30
+ processor.run(SurveyFlow, :main_menu)
31
+ end
32
+
33
+ def whatsapp_endpoint
34
+ processor = FlowChat::Processor.new(self) do |config|
35
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
36
+ config.use_session_store FlowChat::Session::CacheSessionStore
37
+
38
+ # Example 2: Multi-tenant session IDs
39
+ config.use_session_config do |context|
40
+ tenant_id = extract_tenant_from_request(context)
41
+ user_id = context["request.user_id"] || context["request.msisdn"]
42
+ flow_name = context["flow.name"]
43
+
44
+ "tenant_#{tenant_id}_flow_#{flow_name}_user_#{hash_identifier(user_id)}"
45
+ end
46
+ end
47
+
48
+ processor.run(CustomerSupportFlow, :handle_inquiry)
49
+ end
50
+
51
+ def http_endpoint
52
+ processor = FlowChat::Processor.new(self) do |config|
53
+ config.use_gateway FlowChat::Http::Gateway::Simple
54
+ config.use_session_store FlowChat::Session::CacheSessionStore
55
+
56
+ # Example 3: API session with custom expiration tracking
57
+ config.use_session_config do |context|
58
+ api_key = context.controller.request.headers["X-API-Key"]
59
+ request_id = context["request.id"]
60
+
61
+ # Include API key hash for session isolation per API client
62
+ "api_#{hash_identifier(api_key)}_req_#{request_id}"
63
+ end
64
+ end
65
+
66
+ processor.run(ApiFlow, :handle_request)
67
+ end
68
+
69
+ private
70
+
71
+ def hash_phone(phone)
72
+ require "digest"
73
+ Digest::SHA256.hexdigest(phone)[0, 8]
74
+ end
75
+
76
+ def hash_identifier(identifier)
77
+ require "digest"
78
+ Digest::SHA256.hexdigest(identifier.to_s)[0, 8]
79
+ end
80
+
81
+ def extract_tenant_from_request(context)
82
+ # Extract tenant from subdomain or header
83
+ request = context.controller&.request
84
+ return "default" unless request
85
+
86
+ host = request.host
87
+ subdomain = host.split(".").first
88
+ subdomain if subdomain != "www"
89
+ end
90
+ end
91
+
92
+ # Example flows for demonstration
93
+ class SurveyFlow < FlowChat::Flow
94
+ def main_menu
95
+ app.screen(:menu) { |p| p.ask "Welcome! Choose an option:", choices: ["Survey", "Exit"] }
96
+ end
97
+ end
98
+
99
+ class CustomerSupportFlow < FlowChat::Flow
100
+ def handle_inquiry
101
+ app.screen(:inquiry) { |p| p.ask "How can we help you today?" }
102
+ end
103
+ end
104
+
105
+ class ApiFlow < FlowChat::Flow
106
+ def handle_request
107
+ app.screen(:request) { |p| p.ask "API request received. Provide data:" }
108
+ end
109
+ end
110
+
111
+ # Note: The custom session ID proc approach provides:
112
+ # - Complete control over session ID format
113
+ # - Access to full context (request data, flow info, etc.)
114
+ # - Ability to implement complex business logic
115
+ # - Support for multi-tenancy, API authentication, etc.
116
+ #
117
+ # The proc should return a string that will be used as the session ID.
118
+ # Make sure the returned ID is unique for your use case to avoid
119
+ # session collisions.