flow_chat 0.8.2 → 0.9.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 (91) 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/release.yml +56 -0
  5. data/.standard.yml +4 -0
  6. data/CHANGELOG.md +22 -0
  7. data/CLAUDE.md +327 -0
  8. data/CONTRIBUTING.md +134 -0
  9. data/Gemfile +1 -0
  10. data/README.md +290 -105
  11. data/Rakefile +5 -1
  12. data/SECURITY.md +42 -349
  13. data/docs/architecture.md +510 -0
  14. data/docs/async-background-processing.md +298 -0
  15. data/docs/configuration.md +556 -226
  16. data/docs/factory-pattern.md +355 -0
  17. data/docs/gateway-context-variables.md +171 -0
  18. data/docs/gateway-development.md +723 -0
  19. data/docs/getting-started.md +429 -0
  20. data/docs/instrumentation.md +264 -153
  21. data/docs/platforms/telegram.md +1013 -0
  22. data/docs/platforms/ussd.md +693 -0
  23. data/docs/platforms/whatsapp.md +1395 -0
  24. data/docs/testing.md +243 -365
  25. data/examples/custom_session_id_example.rb +119 -0
  26. data/examples/http_controller.rb +11 -11
  27. data/examples/intercom_configuration_example.rb +118 -0
  28. data/examples/intercom_controller.rb +194 -0
  29. data/examples/multi_tenant_whatsapp_controller.rb +4 -4
  30. data/examples/ussd_controller.rb +9 -9
  31. data/examples/whatsapp_controller.rb +2 -2
  32. data/flow_chat.gemspec +4 -0
  33. data/lib/flow_chat/{base_app.rb → app.rb} +16 -9
  34. data/lib/flow_chat/async_job.rb +176 -0
  35. data/lib/flow_chat/config.rb +10 -30
  36. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  37. data/lib/flow_chat/factory.rb +94 -0
  38. data/lib/flow_chat/gateway_async_support.rb +106 -0
  39. data/lib/flow_chat/generic_async_job.rb +30 -0
  40. data/lib/flow_chat/http/gateway/simple.rb +81 -33
  41. data/lib/flow_chat/http/renderer.rb +3 -3
  42. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  43. data/lib/flow_chat/instrumentation.rb +23 -0
  44. data/lib/flow_chat/intercom/client.rb +155 -0
  45. data/lib/flow_chat/intercom/configuration.rb +149 -0
  46. data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
  47. data/lib/flow_chat/intercom/renderer.rb +71 -0
  48. data/lib/flow_chat/phone_number_util.rb +37 -35
  49. data/lib/flow_chat/processor.rb +188 -0
  50. data/lib/flow_chat/renderers/markdown_support.rb +58 -0
  51. data/lib/flow_chat/session/middleware.rb +25 -9
  52. data/lib/flow_chat/telegram/client.rb +240 -0
  53. data/lib/flow_chat/telegram/configuration.rb +118 -0
  54. data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
  55. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
  56. data/lib/flow_chat/telegram/renderer.rb +125 -0
  57. data/lib/flow_chat/telegram.rb +7 -0
  58. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  59. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  60. data/lib/flow_chat/ussd/renderer.rb +1 -1
  61. data/lib/flow_chat/version.rb +1 -1
  62. data/lib/flow_chat/whatsapp/client.rb +144 -13
  63. data/lib/flow_chat/whatsapp/configuration.rb +1 -1
  64. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +132 -96
  65. data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
  66. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
  67. data/lib/flow_chat/whatsapp/renderer.rb +145 -11
  68. data/lib/flow_chat.rb +11 -1
  69. data/lib/tasks/release.rake +165 -0
  70. metadata +84 -25
  71. data/docs/flows.md +0 -320
  72. data/docs/http-gateway-protocol.md +0 -432
  73. data/docs/images/simulator.png +0 -0
  74. data/docs/media.md +0 -153
  75. data/docs/sessions.md +0 -433
  76. data/docs/ussd-setup.md +0 -322
  77. data/docs/whatsapp-setup.md +0 -162
  78. data/examples/whatsapp_message_job.rb +0 -113
  79. data/lib/flow_chat/base_processor.rb +0 -146
  80. data/lib/flow_chat/http/app.rb +0 -6
  81. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  82. data/lib/flow_chat/http/processor.rb +0 -33
  83. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  84. data/lib/flow_chat/ussd/app.rb +0 -6
  85. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  86. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  87. data/lib/flow_chat/ussd/processor.rb +0 -39
  88. data/lib/flow_chat/whatsapp/app.rb +0 -29
  89. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  90. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  91. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
@@ -0,0 +1,693 @@
1
+ # USSD Development with FlowChat
2
+
3
+ USSD (Unstructured Supplementary Service Data) enables interactive menu-driven applications accessible from any mobile phone. FlowChat provides comprehensive USSD support with automatic pagination, choice mapping, and session management.
4
+
5
+ ## Supported USSD Gateways
6
+
7
+ FlowChat's pluggable architecture supports multiple USSD gateways:
8
+
9
+ | Gateway | Provider | Status | Features |
10
+ |---------|----------|---------|----------|
11
+ | **Nalo** | Nalo USSD Platform | ✅ Active | Pagination, choice mapping, session management |
12
+ | **Custom** | Your Implementation | 🔧 Build Your Own | Full gateway interface support |
13
+
14
+ ```ruby
15
+ # Built-in Nalo gateway (included with FlowChat)
16
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
17
+
18
+ # Example custom gateway (you would build this)
19
+ config.use_gateway YourCompany::Ussd::Gateway::MTN, mtn_config
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ### Basic USSD Controller
25
+
26
+ ```ruby
27
+ class UssdController < ApplicationController
28
+ skip_forgery_protection
29
+
30
+ def process_request
31
+ processor = FlowChat::Processor.new(self) do |config|
32
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
33
+ config.use_session_store FlowChat::Session::CacheSessionStore
34
+
35
+ # USSD-optimized session configuration
36
+ config.use_session_config(
37
+ boundaries: [:flow, :platform], # Isolate by flow and platform
38
+ identifier: :msisdn # Use phone number for sessions
39
+ )
40
+ end
41
+
42
+ processor.run MenuFlow, :main_menu
43
+ end
44
+ end
45
+ ```
46
+
47
+ ### Simple USSD Flow
48
+
49
+ ```ruby
50
+ class MenuFlow < FlowChat::Flow
51
+ def main_menu
52
+ choice = app.screen(:menu) do |prompt|
53
+ prompt.select "Welcome! Choose:", {
54
+ "1" => "Account Balance",
55
+ "2" => "Transfer Money",
56
+ "3" => "Buy Airtime",
57
+ "4" => "Contact Support"
58
+ }
59
+ end
60
+
61
+ case choice
62
+ when "1" then show_balance
63
+ when "2" then transfer_money
64
+ when "3" then buy_airtime
65
+ when "4" then show_support
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def show_balance
72
+ balance = get_account_balance(app.msisdn)
73
+ app.say "Balance: UGX #{balance}\nThank you!"
74
+ end
75
+
76
+ def transfer_money
77
+ recipient = app.screen(:recipient) do |prompt|
78
+ prompt.ask "Enter recipient number:",
79
+ validate: ->(input) {
80
+ return "Invalid number" unless input.match?(/^0\d{9}$/)
81
+ nil
82
+ }
83
+ end
84
+
85
+ amount = app.screen(:amount) do |prompt|
86
+ prompt.ask "Enter amount:",
87
+ validate: ->(input) {
88
+ amt = input.to_i
89
+ return "Minimum UGX 500" if amt < 500
90
+ return "Maximum UGX 1,000,000" if amt > 1_000_000
91
+ nil
92
+ },
93
+ transform: ->(input) { input.to_i }
94
+ end
95
+
96
+ confirmed = app.screen(:confirm) do |prompt|
97
+ prompt.yes? "Send UGX #{amount} to #{recipient}?"
98
+ end
99
+
100
+ if confirmed
101
+ transfer_id = process_transfer(app.msisdn, recipient, amount)
102
+ app.say "Transfer successful!\nID: #{transfer_id}"
103
+ else
104
+ app.say "Transfer cancelled"
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ ## USSD-Specific Features
111
+
112
+ ### Automatic Pagination
113
+
114
+ Long messages are automatically split into pages with navigation options:
115
+
116
+ ```ruby
117
+ # Configuration
118
+ FlowChat::Config.ussd.pagination_page_size = 160 # Characters per page
119
+ FlowChat::Config.ussd.pagination_next_option = "#" # Next page option
120
+ FlowChat::Config.ussd.pagination_back_option = "0" # Previous page option
121
+
122
+ # Long content automatically paginated
123
+ def show_transaction_history
124
+ transactions = get_user_transactions(app.msisdn)
125
+
126
+ history = transactions.map.with_index(1) do |txn, i|
127
+ "#{i}. #{txn.date} - #{txn.type}\nAmount: #{txn.amount}\nRef: #{txn.reference}"
128
+ end.join("\n\n")
129
+
130
+ # FlowChat automatically paginates this long content
131
+ app.say "Transaction History:\n\n#{history}"
132
+ end
133
+ ```
134
+
135
+ ### Choice Mapping
136
+
137
+ USSD choices are automatically mapped between user selections and actual values:
138
+
139
+ ```ruby
140
+ def product_selection
141
+ # User sees numbered options, your code gets meaningful values
142
+ product = app.screen(:product) do |prompt|
143
+ prompt.select "Choose product:", {
144
+ "airtime" => "Buy Airtime",
145
+ "data" => "Buy Data Bundle",
146
+ "voice" => "Voice Bundle",
147
+ "sms" => "SMS Bundle"
148
+ }
149
+ end
150
+
151
+ # product variable contains "airtime", "data", etc.
152
+ case product
153
+ when "airtime" then buy_airtime_flow
154
+ when "data" then buy_data_flow
155
+ # ...
156
+ end
157
+ end
158
+ ```
159
+
160
+ ### Session Management
161
+
162
+ USSD sessions are typically short-lived but can be configured for different behaviors:
163
+
164
+ ```ruby
165
+ # Ephemeral sessions (default for USSD)
166
+ config.use_session_config(identifier: :request_id)
167
+
168
+ # Durable sessions (survive USSD timeouts)
169
+ config.use_durable_sessions # Uses :msisdn identifier
170
+
171
+ # Custom session configuration
172
+ config.use_session_config(
173
+ boundaries: [:flow, :platform, :gateway], # Session isolation
174
+ identifier: :msisdn, # Use phone number
175
+ hash_identifiers: true # Hash for privacy
176
+ )
177
+ ```
178
+
179
+ ## Advanced USSD Patterns
180
+
181
+ ### Multi-Step Validation
182
+
183
+ ```ruby
184
+ def registration_flow
185
+ # Phone number validation with formatting
186
+ phone = app.screen(:phone) do |prompt|
187
+ prompt.ask "Enter phone number:",
188
+ validate: ->(input) {
189
+ # Remove common prefixes/formatting
190
+ clean = input.gsub(/[\s\-\(\)]/, '')
191
+ clean = clean.sub(/^(\+256|256|0)/, '')
192
+
193
+ return "Invalid format" unless clean.match?(/^\d{9}$/)
194
+ nil
195
+ },
196
+ transform: ->(input) {
197
+ clean = input.gsub(/[\s\-\(\)]/, '').sub(/^(\+256|256|0)/, '')
198
+ "+256#{clean}"
199
+ }
200
+ end
201
+
202
+ # Name validation
203
+ name = app.screen(:name) do |prompt|
204
+ prompt.ask "Enter full name:",
205
+ validate: ->(input) {
206
+ return "Name too short" if input.length < 2
207
+ return "Name too long" if input.length > 50
208
+ return "Invalid characters" unless input.match?(/^[a-zA-Z\s\.]+$/)
209
+ nil
210
+ },
211
+ transform: ->(input) { input.strip.titleize }
212
+ end
213
+
214
+ # Create account
215
+ account_id = create_account(phone, name)
216
+ app.say "Welcome #{name}!\nAccount: #{account_id}"
217
+ end
218
+ ```
219
+
220
+ ### Conditional Menu Systems
221
+
222
+ ```ruby
223
+ def main_menu
224
+ user = get_user_profile(app.msisdn)
225
+
226
+ if user.nil?
227
+ registration_flow
228
+ elsif user.account_status == 'suspended'
229
+ suspended_account_menu
230
+ elsif user.account_type == 'premium'
231
+ premium_menu
232
+ else
233
+ standard_menu
234
+ end
235
+ end
236
+
237
+ def premium_menu
238
+ choice = app.screen(:premium_menu) do |prompt|
239
+ prompt.select "Premium Menu:", {
240
+ "1" => "💳 Account Balance",
241
+ "2" => "💸 Transfer Money",
242
+ "3" => "📱 Buy Bundles",
243
+ "4" => "📊 Investment Dashboard",
244
+ "5" => "🏆 Premium Support",
245
+ "0" => "Exit"
246
+ }
247
+ end
248
+
249
+ # Handle premium-specific options
250
+ end
251
+
252
+ def standard_menu
253
+ choice = app.screen(:standard_menu) do |prompt|
254
+ prompt.select "Main Menu:", {
255
+ "1" => "Check Balance",
256
+ "2" => "Transfer Money",
257
+ "3" => "Buy Airtime",
258
+ "4" => "Support",
259
+ "0" => "Exit"
260
+ }
261
+ end
262
+ end
263
+ ```
264
+
265
+ ### Error Handling and Recovery
266
+
267
+ ```ruby
268
+ def transfer_money
269
+ begin
270
+ recipient = app.screen(:recipient) do |prompt|
271
+ prompt.ask "Recipient number:",
272
+ validate: method(:validate_phone_number)
273
+ end
274
+
275
+ amount = app.screen(:amount) do |prompt|
276
+ prompt.ask "Amount (UGX):",
277
+ validate: method(:validate_amount),
278
+ transform: ->(input) { input.to_i }
279
+ end
280
+
281
+ # Check balance before confirmation
282
+ balance = get_balance(app.msisdn)
283
+ if amount > balance
284
+ app.say "Insufficient funds.\nBalance: UGX #{balance}\nRequired: UGX #{amount}"
285
+ return
286
+ end
287
+
288
+ confirmed = app.screen(:confirm) do |prompt|
289
+ prompt.yes? "Send UGX #{amount} to #{recipient}?\nFee: UGX #{calculate_fee(amount)}"
290
+ end
291
+
292
+ if confirmed
293
+ transfer_id = process_transfer(app.msisdn, recipient, amount)
294
+ app.say "Transfer successful!\nRef: #{transfer_id}\nNew balance: UGX #{get_balance(app.msisdn)}"
295
+ else
296
+ app.say "Transfer cancelled"
297
+ end
298
+
299
+ rescue InsufficientFundsError => e
300
+ app.say "Transfer failed: Insufficient funds"
301
+ rescue NetworkError => e
302
+ app.say "Network error. Please try again.\nRef: #{SecureRandom.hex(4)}"
303
+ rescue => e
304
+ Rails.logger.error "Transfer error: #{e.message}"
305
+ app.say "Service temporarily unavailable.\nPlease try again later."
306
+ end
307
+ end
308
+
309
+ private
310
+
311
+ def validate_phone_number(input)
312
+ # Normalize input
313
+ clean = input.gsub(/[\s\-\(\)]/, '')
314
+ clean = clean.sub(/^(\+256|256|0)/, '')
315
+
316
+ return "Enter 9 digits" unless clean.match?(/^\d{9}$/)
317
+ return "Cannot send to yourself" if "+256#{clean}" == app.msisdn
318
+ nil
319
+ end
320
+
321
+ def validate_amount(input)
322
+ amount = input.to_i
323
+ return "Enter numbers only" if amount == 0 && input != "0"
324
+ return "Minimum UGX 500" if amount < 500
325
+ return "Maximum UGX 2,000,000" if amount > 2_000_000
326
+ nil
327
+ end
328
+ ```
329
+
330
+ ### Back Navigation
331
+
332
+ ```ruby
333
+ def multi_level_menu
334
+ main_choice = app.screen(:main) do |prompt|
335
+ prompt.select "Main Menu:", {
336
+ "1" => "Financial Services",
337
+ "2" => "Lifestyle Services",
338
+ "3" => "Account Settings",
339
+ "0" => "Exit"
340
+ }
341
+ end
342
+
343
+ case main_choice
344
+ when "1"
345
+ financial_services_menu
346
+ when "2"
347
+ lifestyle_services_menu
348
+ when "3"
349
+ account_settings_menu
350
+ when "0"
351
+ app.say "Thank you for using our service!"
352
+ end
353
+ end
354
+
355
+ def financial_services_menu
356
+ choice = app.screen(:financial) do |prompt|
357
+ prompt.select "Financial Services:", {
358
+ "1" => "Transfer Money",
359
+ "2" => "Pay Bills",
360
+ "3" => "Savings Account",
361
+ "9" => "Back to Main Menu",
362
+ "0" => "Exit"
363
+ }
364
+ end
365
+
366
+ case choice
367
+ when "1" then transfer_money
368
+ when "2" then pay_bills
369
+ when "3" then savings_menu
370
+ when "9"
371
+ # Clear current screen and go back
372
+ app.session.delete(:financial)
373
+ multi_level_menu
374
+ when "0"
375
+ app.say "Thank you!"
376
+ end
377
+ end
378
+
379
+ # Alternative: Use app.go_back for automatic navigation
380
+ def with_go_back
381
+ if user_wants_to_go_back?
382
+ app.go_back # Automatically handles navigation stack
383
+ end
384
+ end
385
+ ```
386
+
387
+ ## USSD Configuration
388
+
389
+ ### Global USSD Settings
390
+
391
+ ```ruby
392
+ # config/initializers/flow_chat.rb
393
+ FlowChat::Config.ussd.pagination_page_size = 140 # SMS character limit
394
+ FlowChat::Config.ussd.pagination_next_option = "#" # Next page key
395
+ FlowChat::Config.ussd.pagination_back_option = "0" # Previous page key
396
+ FlowChat::Config.ussd.pagination_next_text = "More" # Next page text
397
+ FlowChat::Config.ussd.pagination_back_text = "Back" # Previous page text
398
+
399
+ # Validation settings
400
+ FlowChat::Config.combine_validation_error_with_message = true # Show original + error
401
+ ```
402
+
403
+ ### Per-Request Configuration
404
+
405
+ ```ruby
406
+ def process_request
407
+ # Configure pagination based on network
408
+ network = detect_network(request)
409
+
410
+ case network
411
+ when :mtn
412
+ FlowChat::Config.ussd.pagination_page_size = 160
413
+ when :airtel
414
+ FlowChat::Config.ussd.pagination_page_size = 140
415
+ when :africel
416
+ FlowChat::Config.ussd.pagination_page_size = 120
417
+ end
418
+
419
+ processor = FlowChat::Processor.new(self) do |config|
420
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
421
+ config.use_session_store FlowChat::Session::CacheSessionStore
422
+ end
423
+
424
+ processor.run MenuFlow, :main_menu
425
+ end
426
+ ```
427
+
428
+ ## Testing USSD Applications
429
+
430
+ ### Using the Simulator
431
+
432
+ ```ruby
433
+ # In rails console or test
434
+ simulator = FlowChat::Simulator.new(MenuFlow, :main_menu)
435
+
436
+ # Simulate USSD session
437
+ simulator.start
438
+ # => "Welcome! Choose:\n1. Account Balance\n2. Transfer Money\n..."
439
+
440
+ simulator.send_message("2") # Select transfer money
441
+ # => "Enter recipient number:"
442
+
443
+ simulator.send_message("0701234567")
444
+ # => "Enter amount:"
445
+
446
+ simulator.send_message("5000")
447
+ # => "Send UGX 5000 to 0701234567?\n1. Yes\n2. No"
448
+
449
+ simulator.send_message("1") # Confirm
450
+ # => "Transfer successful!\nRef: TXN123456"
451
+ ```
452
+
453
+ ### Integration Testing
454
+
455
+ ```ruby
456
+ # test/integration/ussd_flow_test.rb
457
+ class UssdFlowTest < ActionDispatch::IntegrationTest
458
+ def test_complete_transfer_flow
459
+ # Simulate Nalo USSD webhook
460
+ post ussd_path, params: {
461
+ msisdn: "256701234567",
462
+ text: "",
463
+ session_id: "test_session_123"
464
+ }
465
+
466
+ assert_response :success
467
+ assert_includes response.body, "Welcome! Choose:"
468
+
469
+ # Select transfer money
470
+ post ussd_path, params: {
471
+ msisdn: "256701234567",
472
+ text: "2",
473
+ session_id: "test_session_123"
474
+ }
475
+
476
+ assert_includes response.body, "Enter recipient number:"
477
+
478
+ # Continue flow...
479
+ end
480
+ end
481
+ ```
482
+
483
+ ## Network-Specific Considerations
484
+
485
+ ### Character Limits
486
+
487
+ Different networks have different USSD character limits:
488
+
489
+ ```ruby
490
+ NETWORK_LIMITS = {
491
+ mtn: 160,
492
+ airtel: 140,
493
+ africel: 120,
494
+ utl: 140
495
+ }.freeze
496
+
497
+ def adjust_pagination_for_network(network)
498
+ limit = NETWORK_LIMITS[network] || 140
499
+ FlowChat::Config.ussd.pagination_page_size = limit
500
+ end
501
+ ```
502
+
503
+ ### Network-Specific Features
504
+
505
+ ```ruby
506
+ def network_aware_menu
507
+ network = detect_network(app.msisdn)
508
+
509
+ base_options = {
510
+ "1" => "Check Balance",
511
+ "2" => "Transfer Money",
512
+ "3" => "Buy Airtime"
513
+ }
514
+
515
+ # Add network-specific options
516
+ case network
517
+ when :mtn
518
+ base_options["4"] = "MTN Mobile Money"
519
+ base_options["5"] = "MTN Packages"
520
+ when :airtel
521
+ base_options["4"] = "Airtel Money"
522
+ base_options["5"] = "Airtel Packages"
523
+ end
524
+
525
+ choice = app.screen(:network_menu) do |prompt|
526
+ prompt.select "Services:", base_options
527
+ end
528
+
529
+ handle_choice(choice, network)
530
+ end
531
+ ```
532
+
533
+ ## Performance Optimization
534
+
535
+ ### Efficient Database Queries
536
+
537
+ ```ruby
538
+ def show_transaction_history
539
+ # Efficient pagination for large datasets
540
+ page_size = 5
541
+ offset = (current_page - 1) * page_size
542
+
543
+ transactions = Transaction
544
+ .where(msisdn: app.msisdn)
545
+ .order(created_at: :desc)
546
+ .limit(page_size)
547
+ .offset(offset)
548
+ .pluck(:date, :type, :amount, :reference)
549
+
550
+ if transactions.empty?
551
+ app.say "No transactions found"
552
+ return
553
+ end
554
+
555
+ history = transactions.map.with_index(offset + 1) do |(date, type, amount, ref), i|
556
+ "#{i}. #{date.strftime('%d/%m')} #{type}\nUGX #{amount}\n#{ref}"
557
+ end.join("\n\n")
558
+
559
+ app.say "Transactions:\n#{history}"
560
+ end
561
+ ```
562
+
563
+ ### Caching Strategies
564
+
565
+ ```ruby
566
+ def get_exchange_rates
567
+ # Cache exchange rates for 1 hour
568
+ Rails.cache.fetch("exchange_rates", expires_in: 1.hour) do
569
+ fetch_exchange_rates_from_api
570
+ end
571
+ end
572
+
573
+ def get_user_profile(msisdn)
574
+ # Cache user profile for session duration
575
+ app.session.get("user_profile") || begin
576
+ profile = User.find_by(msisdn: msisdn)
577
+ app.session.set("user_profile", profile)
578
+ profile
579
+ end
580
+ end
581
+ ```
582
+
583
+ ## Security Best Practices
584
+
585
+ ### Input Sanitization
586
+
587
+ ```ruby
588
+ def secure_input_handling
589
+ # Always validate and sanitize input
590
+ amount = app.screen(:amount) do |prompt|
591
+ prompt.ask "Enter amount:",
592
+ validate: ->(input) {
593
+ # Remove any non-numeric characters
594
+ clean = input.gsub(/[^\d]/, '')
595
+ return "Enter numbers only" if clean.empty?
596
+
597
+ amount = clean.to_i
598
+ return "Amount too small" if amount < 100
599
+ return "Amount too large" if amount > 10_000_000
600
+ nil
601
+ },
602
+ transform: ->(input) { input.gsub(/[^\d]/, '').to_i }
603
+ end
604
+ end
605
+ ```
606
+
607
+ ### Session Security
608
+
609
+ ```ruby
610
+ # Hash sensitive identifiers
611
+ config.use_session_config(
612
+ identifier: :msisdn,
613
+ hash_identifiers: true # Phone numbers are hashed
614
+ )
615
+
616
+ # Implement session timeouts
617
+ class SecureSessionStore < FlowChat::Session::CacheSessionStore
618
+ def get(key)
619
+ data = super
620
+ if data && session_expired?
621
+ delete_all_session_data
622
+ return nil
623
+ end
624
+ data
625
+ end
626
+
627
+ private
628
+
629
+ def session_expired?
630
+ last_activity = @context.session.get("last_activity")
631
+ return true unless last_activity
632
+
633
+ Time.current - Time.parse(last_activity) > 10.minutes
634
+ end
635
+ end
636
+ ```
637
+
638
+ ## Troubleshooting
639
+
640
+ ### Common USSD Issues
641
+
642
+ 1. **Session Loss**
643
+ ```ruby
644
+ # Use durable sessions for longer flows
645
+ config.use_durable_sessions
646
+
647
+ # Or implement session recovery
648
+ def recover_session
649
+ if app.session.get(:current_screen).nil?
650
+ # Restart from main menu
651
+ main_menu
652
+ end
653
+ end
654
+ ```
655
+
656
+ 2. **Character Encoding**
657
+ ```ruby
658
+ # Ensure proper encoding for special characters
659
+ def safe_message(text)
660
+ text.encode('UTF-8', invalid: :replace, undef: :replace)
661
+ end
662
+ ```
663
+
664
+ 3. **Timeout Handling**
665
+ ```ruby
666
+ def handle_timeout
667
+ app.say "Session timeout.\nDial *123# to continue"
668
+ rescue FlowChat::Interrupt::SessionTimeout
669
+ handle_timeout
670
+ end
671
+ ```
672
+
673
+ ### Debugging
674
+
675
+ Enable detailed logging:
676
+
677
+ ```ruby
678
+ # config/initializers/flow_chat.rb
679
+ FlowChat::Config.logger.level = Logger::DEBUG
680
+
681
+ # In your flow
682
+ Rails.logger.debug "USSD Debug - Screen: #{app.navigation_stack.last}"
683
+ Rails.logger.debug "USSD Debug - Input: #{app.input.inspect}"
684
+ Rails.logger.debug "USSD Debug - Session: #{app.session.inspect}"
685
+ ```
686
+
687
+ ## Next Steps
688
+
689
+ - **[Gateway Development](../gateway-development.md)** - Build custom USSD gateways
690
+ - **[Session Management](../session-management.md)** - Advanced session configuration
691
+ - **[Multi-Platform Development](multi-platform.md)** - Share flows across platforms
692
+
693
+ See the complete USSD documentation for advanced patterns, configuration options, and best practices.