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
@@ -4,11 +4,16 @@ module FlowChat
4
4
  module Ussd
5
5
  module Gateway
6
6
  class Nsano
7
+ include FlowChat::Instrumentation
8
+
9
+ attr_reader :context
10
+
7
11
  def initialize(app)
8
12
  @app = app
9
13
  end
10
14
 
11
15
  def call(context)
16
+ @context = context
12
17
  controller = context["controller"]
13
18
  controller.request
14
19
 
@@ -17,6 +22,35 @@ module FlowChat
17
22
 
18
23
  # Set a basic message_id (can be enhanced based on actual Nsano implementation)
19
24
  context["request.message_id"] = SecureRandom.uuid
25
+ context["request.platform"] = :ussd
26
+
27
+ # TODO: Implement Nsano-specific parameter parsing
28
+ # For now, add basic instrumentation structure for when this is implemented
29
+
30
+ # Placeholder instrumentation - indicates Nsano implementation is needed
31
+ instrument(Events::MESSAGE_RECEIVED, {
32
+ from: "TODO", # Would be parsed from Nsano params
33
+ message: "TODO", # Would be actual user input
34
+ session_id: "TODO", # Would be Nsano session ID
35
+ gateway: :nsano,
36
+ platform: :ussd,
37
+ timestamp: context["request.timestamp"]
38
+ })
39
+
40
+ # Process request with placeholder app call
41
+ _, _, _, _ = @app.call(context) if @app
42
+
43
+ # Placeholder response instrumentation
44
+ instrument(Events::MESSAGE_SENT, {
45
+ to: "TODO", # Would be actual phone number
46
+ session_id: "TODO", # Would be Nsano session ID
47
+ message: "TODO", # Would be actual response message
48
+ message_type: "prompt", # Would depend on actual response type
49
+ gateway: :nsano,
50
+ platform: :ussd,
51
+ content_length: 0, # Would be actual content length
52
+ timestamp: context["request.timestamp"]
53
+ })
20
54
 
21
55
  # input = context["rack.input"].read
22
56
  # context["rack.input"].rewind
@@ -25,7 +59,7 @@ module FlowChat
25
59
  # if params["network"].present? && params["UserSessionID"].present?
26
60
  # request_id = "nsano::request_id::#{params["UserSessionID"]}"
27
61
  # context["ussd.request"] = {
28
- # provider: :nsano,
62
+ # gateway: :nsano,
29
63
  # network: params["network"].to_sym,
30
64
  # msisdn: Phonelib.parse(params["msisdn"]).e164,
31
65
  # type: Config.cache&.read(request_id).present? ? :response : :initial,
@@ -37,7 +71,7 @@ module FlowChat
37
71
 
38
72
  # status, headers, response = @app.call(context)
39
73
 
40
- # if context["ussd.response"].present? && context["ussd.request"][:provider] == :nsano
74
+ # if context["ussd.response"].present? && context["ussd.request"][:gateway] == :nsano
41
75
  # if context["ussd.response"][:type] == :terminal
42
76
  # Config.cache&.write(request_id, nil)
43
77
  # else
@@ -0,0 +1,109 @@
1
+ module FlowChat
2
+ module Ussd
3
+ module Middleware
4
+ class ChoiceMapper
5
+ def initialize(app)
6
+ @app = app
7
+ FlowChat.logger.debug { "Ussd::ChoiceMapper: Initialized USSD choice mapping middleware" }
8
+ end
9
+
10
+ def call(context)
11
+ @context = context
12
+ @session = context.session
13
+
14
+ session_id = context["session.id"]
15
+ FlowChat.logger.debug { "Ussd::ChoiceMapper: Processing request for session #{session_id}" }
16
+
17
+ if intercept?
18
+ FlowChat.logger.info { "Ussd::ChoiceMapper: Intercepting request for choice resolution - session #{session_id}" }
19
+ handle_choice_input
20
+ end
21
+
22
+ # Clear choice mapping state for new flows
23
+ clear_choice_state_if_needed
24
+ type, prompt, choices, media = @app.call(context)
25
+
26
+ if choices.present?
27
+ FlowChat.logger.debug { "Ussd::ChoiceMapper: Found choices, creating number mapping" }
28
+ choices = create_numbered_mapping(choices)
29
+ end
30
+
31
+ [type, prompt, choices, media]
32
+ end
33
+
34
+ private
35
+
36
+ def intercept?
37
+ # Intercept if we have choice mapping state and user input is a number that maps to a choice
38
+ choice_mapping = get_choice_mapping
39
+ should_intercept = choice_mapping.present? &&
40
+ @context.input.present? &&
41
+ choice_mapping.key?(@context.input.to_s)
42
+
43
+ if should_intercept
44
+ FlowChat.logger.debug { "Ussd::ChoiceMapper: Intercepting - input: #{@context.input}, mapped to: #{choice_mapping[@context.input.to_s]}" }
45
+ end
46
+
47
+ should_intercept
48
+ end
49
+
50
+ def handle_choice_input
51
+ choice_mapping = get_choice_mapping
52
+ original_choice = choice_mapping[@context.input.to_s]
53
+
54
+ FlowChat.logger.info { "Ussd::ChoiceMapper: Resolving choice input #{@context.input} to #{original_choice}" }
55
+
56
+ # Replace the numeric input with the original choice
57
+ @context.input = original_choice
58
+ end
59
+
60
+ def create_numbered_mapping(choices)
61
+ # Choices are always a hash after normalize_choices
62
+ numbered_choices = {}
63
+ choice_mapping = {}
64
+
65
+ choices.each_with_index do |(key, value), index|
66
+ number = (index + 1).to_s
67
+ numbered_choices[number] = value
68
+ choice_mapping[number] = key.to_s
69
+ end
70
+
71
+ store_choice_mapping(choice_mapping)
72
+ FlowChat.logger.debug { "Ussd::ChoiceMapper: Created mapping: #{choice_mapping}" }
73
+ numbered_choices
74
+ end
75
+
76
+ def store_choice_mapping(mapping)
77
+ @session.set("ussd.choice_mapping", mapping)
78
+ FlowChat.logger.debug { "Ussd::ChoiceMapper: Stored choice mapping: #{mapping}" }
79
+ end
80
+
81
+ def get_choice_mapping
82
+ @session.get("ussd.choice_mapping") || {}
83
+ end
84
+
85
+ def clear_choice_mapping
86
+ @session.delete("ussd.choice_mapping")
87
+ FlowChat.logger.debug { "Ussd::ChoiceMapper: Cleared choice mapping" }
88
+ end
89
+
90
+ def clear_choice_state_if_needed
91
+ # Clear choice mapping if this is a new flow (no input or fresh start)
92
+ if @context.input.blank? || should_clear_for_new_flow?
93
+ clear_choice_mapping
94
+ end
95
+ end
96
+
97
+ def should_clear_for_new_flow?
98
+ # Clear mapping if this input doesn't match any stored mapping
99
+ # This indicates we're in a new flow step
100
+ choice_mapping = get_choice_mapping
101
+ return false if choice_mapping.empty?
102
+
103
+ # If input is present but doesn't match any mapping, we're in a new flow
104
+ @context.input.present? && !choice_mapping.key?(@context.input.to_s)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -4,22 +4,44 @@ module FlowChat
4
4
  class Executor
5
5
  def initialize(app)
6
6
  @app = app
7
+ FlowChat.logger.debug { "Ussd::Executor: Initialized USSD executor middleware" }
7
8
  end
8
9
 
9
10
  def call(context)
11
+ flow_class = context.flow
12
+ action = context["flow.action"]
13
+ session_id = context["session.id"]
14
+
15
+ FlowChat.logger.info { "Ussd::Executor: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
16
+
10
17
  ussd_app = build_ussd_app context
11
- flow = context.flow.new ussd_app
12
- flow.send context["flow.action"]
18
+ FlowChat.logger.debug { "Ussd::Executor: USSD app built for flow execution" }
19
+
20
+ flow = flow_class.new ussd_app
21
+ FlowChat.logger.debug { "Ussd::Executor: Flow instance created, invoking #{action} method" }
22
+
23
+ flow.send action
24
+ FlowChat.logger.warn { "Ussd::Executor: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
25
+ raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
13
26
  rescue FlowChat::Interrupt::Prompt => e
27
+ FlowChat.logger.info { "Ussd::Executor: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt.truncate(100)}'" }
28
+ FlowChat.logger.debug { "Ussd::Executor: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
14
29
  [:prompt, e.prompt, e.choices, e.media]
15
30
  rescue FlowChat::Interrupt::Terminate => e
31
+ FlowChat.logger.info { "Ussd::Executor: Flow terminated - Session: #{session_id}, Message: '#{e.prompt.truncate(100)}'" }
32
+ FlowChat.logger.debug { "Ussd::Executor: Destroying session #{session_id}" }
16
33
  context.session.destroy
17
34
  [:terminate, e.prompt, nil, e.media]
35
+ rescue => error
36
+ FlowChat.logger.error { "Ussd::Executor: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
37
+ FlowChat.logger.debug { "Ussd::Executor: Stack trace: #{error.backtrace.join("\n")}" }
38
+ raise
18
39
  end
19
40
 
20
41
  private
21
42
 
22
43
  def build_ussd_app(context)
44
+ FlowChat.logger.debug { "Ussd::Executor: Building USSD app instance" }
23
45
  FlowChat::Ussd::App.new(context)
24
46
  end
25
47
  end
@@ -2,23 +2,44 @@ module FlowChat
2
2
  module Ussd
3
3
  module Middleware
4
4
  class Pagination
5
+ include FlowChat::Instrumentation
6
+
7
+ attr_reader :context
8
+
5
9
  def initialize(app)
6
10
  @app = app
11
+ FlowChat.logger.debug { "Ussd::Pagination: Initialized USSD pagination middleware" }
7
12
  end
8
13
 
9
14
  def call(context)
10
15
  @context = context
11
16
  @session = context.session
12
17
 
18
+ session_id = context["session.id"]
19
+ FlowChat.logger.debug { "Ussd::Pagination: Processing request for session #{session_id}" }
20
+
13
21
  if intercept?
22
+ FlowChat.logger.info { "Ussd::Pagination: Intercepting request for pagination handling - session #{session_id}" }
14
23
  type, prompt = handle_intercepted_request
15
24
  [type, prompt, []]
16
25
  else
26
+ # Clear pagination state for new flows
27
+ if pagination_state.present?
28
+ FlowChat.logger.debug { "Ussd::Pagination: Clearing pagination state for new flow - session #{session_id}" }
29
+ end
17
30
  @session.delete "ussd.pagination"
31
+
18
32
  type, prompt, choices, media = @app.call(context)
19
33
 
20
34
  prompt = FlowChat::Ussd::Renderer.new(prompt, choices: choices, media: media).render
21
- type, prompt = maybe_paginate(type, prompt) if prompt.present?
35
+
36
+ if prompt.present?
37
+ original_length = prompt.length
38
+ type, prompt = maybe_paginate(type, prompt)
39
+ if prompt.length != original_length
40
+ FlowChat.logger.info { "Ussd::Pagination: Content paginated - original: #{original_length} chars, paginated: #{prompt.length} chars" }
41
+ end
42
+ end
22
43
 
23
44
  [type, prompt, []]
24
45
  end
@@ -27,53 +48,91 @@ module FlowChat
27
48
  private
28
49
 
29
50
  def intercept?
30
- pagination_state.present? &&
51
+ should_intercept = pagination_state.present? &&
31
52
  (pagination_state["type"].to_sym == :terminal ||
32
53
  ([FlowChat::Config.ussd.pagination_next_option, FlowChat::Config.ussd.pagination_back_option].include? @context.input))
54
+
55
+ if should_intercept
56
+ FlowChat.logger.debug { "Ussd::Pagination: Intercepting - input: #{@context.input}, pagination type: #{pagination_state["type"]}" }
57
+ end
58
+
59
+ should_intercept
33
60
  end
34
61
 
35
62
  def handle_intercepted_request
36
- FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Intercepted to handle pagination"
63
+ FlowChat.logger.info { "Ussd::Pagination: Handling paginated request" }
37
64
  start, finish, has_more = calculate_offsets
38
65
  type = (pagination_state["type"].to_sym == :terminal && !has_more) ? :terminal : :prompt
39
66
  prompt = pagination_state["prompt"][start..finish] + build_pagination_options(type, has_more)
40
67
  set_pagination_state(current_page, start, finish)
41
68
 
69
+ # Instrument pagination navigation
70
+ instrument(Events::PAGINATION_TRIGGERED, {
71
+ session_id: @context["session.id"],
72
+ current_page: current_page,
73
+ total_pages: calculate_total_pages,
74
+ content_length: pagination_state["prompt"].length,
75
+ page_limit: FlowChat::Config.ussd.pagination_page_size,
76
+ navigation_action: (@context.input == FlowChat::Config.ussd.pagination_next_option) ? "next" : "back"
77
+ })
78
+
79
+ FlowChat.logger.debug { "Ussd::Pagination: Serving page content - start: #{start}, finish: #{finish}, has_more: #{has_more}, type: #{type}" }
42
80
  [type, prompt]
43
81
  end
44
82
 
45
83
  def maybe_paginate(type, prompt)
46
84
  if prompt.length > FlowChat::Config.ussd.pagination_page_size
47
85
  original_prompt = prompt
48
- FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{prompt.length}) exceeds page size (#{FlowChat::Config.ussd.pagination_page_size}). Paginating."
86
+ FlowChat.logger.info { "Ussd::Pagination: Content exceeds page size (#{prompt.length} > #{FlowChat::Config.ussd.pagination_page_size}), initiating pagination" }
87
+
49
88
  slice_end = single_option_slice_size
50
89
  # Ensure we do not cut words and options off in the middle.
51
90
  current_pagebreak = original_prompt[slice_end + 1].blank? ? slice_end : original_prompt[0..slice_end].rindex("\n") || original_prompt[0..slice_end].rindex(" ") || slice_end
91
+
92
+ FlowChat.logger.debug { "Ussd::Pagination: First page break at position #{current_pagebreak}" }
93
+
52
94
  set_pagination_state(1, 0, current_pagebreak, original_prompt, type)
53
95
  prompt = original_prompt[0..current_pagebreak] + "\n\n" + next_option
54
96
  type = :prompt
97
+
98
+ # Instrument initial pagination setup
99
+ total_pages = calculate_total_pages(original_prompt)
100
+ instrument(Events::PAGINATION_TRIGGERED, {
101
+ session_id: @context["session.id"],
102
+ current_page: 1,
103
+ total_pages: total_pages,
104
+ content_length: original_prompt.length,
105
+ page_limit: FlowChat::Config.ussd.pagination_page_size,
106
+ navigation_action: "initial"
107
+ })
108
+
109
+ FlowChat.logger.debug { "Ussd::Pagination: First page prepared with #{prompt.length} characters" }
55
110
  end
56
111
  [type, prompt]
57
112
  end
58
113
 
59
114
  def calculate_offsets
60
115
  page = current_page
116
+
117
+ FlowChat.logger.debug { "Ussd::Pagination: Calculating offsets for page #{page}" }
118
+
61
119
  offset = pagination_state["offsets"][page.to_s]
62
120
  if offset.present?
63
- FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: Reusing cached offset for page: #{page}"
121
+ FlowChat.logger.debug { "Ussd::Pagination: Using cached offset for page #{page}" }
64
122
  start = offset["start"]
65
123
  finish = offset["finish"]
66
124
  has_more = pagination_state["prompt"].length > finish
67
125
  else
68
- FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: Calculating offset for page: #{page}"
126
+ FlowChat.logger.debug { "Ussd::Pagination: Computing new offset for page #{page}" }
69
127
  # We are guaranteed a previous offset because it was set in maybe_paginate
70
128
  previous_page = page - 1
71
129
  previous_offset = pagination_state["offsets"][previous_page.to_s]
72
130
  start = previous_offset["finish"] + 1
73
131
  has_more, len = (pagination_state["prompt"].length > start + single_option_slice_size) ? [true, dual_options_slice_size] : [false, single_option_slice_size]
74
132
  finish = start + len
133
+
75
134
  if start > pagination_state["prompt"].length
76
- FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: No content exists for page: #{page}. Reverting to page: #{page - 1}"
135
+ FlowChat.logger.warn { "Ussd::Pagination: No content for page #{page}, reverting to page #{page - 1}" }
77
136
  page -= 1
78
137
  has_more = false
79
138
  start = previous_offset["start"]
@@ -90,19 +149,26 @@ module FlowChat
90
149
  # We're in the middle of a word, find the last word boundary
91
150
  boundary_pos = slice_text.rindex("\n") || slice_text.rindex(" ")
92
151
  if boundary_pos
152
+ old_finish = finish
93
153
  finish = start + boundary_pos
154
+ FlowChat.logger.debug { "Ussd::Pagination: Adjusted finish for word boundary - #{old_finish} -> #{finish}" }
94
155
  end
95
156
  # If no boundary found, we'll have to break mid-word (fallback)
96
157
  end
97
158
  end
98
159
  end
99
160
  end
161
+
162
+ FlowChat.logger.debug { "Ussd::Pagination: Page #{page} offsets - start: #{start}, finish: #{finish}, has_more: #{has_more}" }
100
163
  [start, finish, has_more]
101
164
  end
102
165
 
103
166
  def build_pagination_options(type, has_more)
104
167
  options_str = ""
105
168
  has_less = current_page > 1
169
+
170
+ FlowChat.logger.debug { "Ussd::Pagination: Building pagination options - type: #{type}, has_more: #{has_more}, has_less: #{has_less}" }
171
+
106
172
  if type.to_sym == :prompt
107
173
  options_str += "\n\n"
108
174
  next_opt = has_more ? next_option : ""
@@ -126,6 +192,7 @@ module FlowChat
126
192
  # We accomodate the 2 newlines and the longest of the options
127
193
  # We subtract an additional 1 to normalize it for slicing
128
194
  @single_option_slice_size = FlowChat::Config.ussd.pagination_page_size - 2 - [next_option.length, back_option.length].max - 1
195
+ FlowChat.logger.debug { "Ussd::Pagination: Calculated single option slice size: #{@single_option_slice_size}" }
129
196
  end
130
197
  @single_option_slice_size
131
198
  end
@@ -135,6 +202,7 @@ module FlowChat
135
202
  # To display both back and next options
136
203
  # We accomodate the 3 newlines and both of the options
137
204
  @dual_options_slice_size = FlowChat::Config.ussd.pagination_page_size - 3 - [next_option.length, back_option.length].sum - 1
205
+ FlowChat.logger.debug { "Ussd::Pagination: Calculated dual options slice size: #{@dual_options_slice_size}" }
138
206
  end
139
207
  @dual_options_slice_size
140
208
  end
@@ -143,8 +211,10 @@ module FlowChat
143
211
  page = pagination_state["page"]
144
212
  if @context.input == FlowChat::Config.ussd.pagination_back_option
145
213
  page -= 1
214
+ FlowChat.logger.debug { "Ussd::Pagination: Moving to previous page: #{page}" }
146
215
  elsif @context.input == FlowChat::Config.ussd.pagination_next_option
147
216
  page += 1
217
+ FlowChat.logger.debug { "Ussd::Pagination: Moving to next page: #{page}" }
148
218
  end
149
219
  [page, 1].max
150
220
  end
@@ -165,8 +235,18 @@ module FlowChat
165
235
  "prompt" => prompt,
166
236
  "type" => type.to_s
167
237
  }
238
+
239
+ FlowChat.logger.debug { "Ussd::Pagination: Saving pagination state - page: #{page}, total_content: #{prompt&.length || 0} chars" }
168
240
  @session.set "ussd.pagination", new_state
169
241
  end
242
+
243
+ def calculate_total_pages(content = nil)
244
+ content ||= pagination_state["prompt"]
245
+ return 1 unless content&.length&.> FlowChat::Config.ussd.pagination_page_size
246
+
247
+ # Rough estimation - actual pages may vary due to word boundaries
248
+ (content.length.to_f / single_option_slice_size).ceil
249
+ end
170
250
  end
171
251
  end
172
252
  end
@@ -1,9 +1,11 @@
1
1
  module FlowChat
2
2
  module Ussd
3
3
  class Processor < FlowChat::BaseProcessor
4
- def use_resumable_sessions
5
- middleware.insert_before 0, FlowChat::Ussd::Middleware::ResumableSession
6
- self
4
+ def use_durable_sessions(cross_gateway: false)
5
+ FlowChat.logger.debug { "Ussd::Processor: Enabling durable sessions via session configuration" }
6
+ use_session_config(
7
+ identifier: :msisdn # Use MSISDN for durable sessions
8
+ )
7
9
  end
8
10
 
9
11
  protected
@@ -13,14 +15,24 @@ module FlowChat
13
15
  end
14
16
 
15
17
  def build_middleware_stack
18
+ FlowChat.logger.debug { "Ussd::Processor: Building USSD middleware stack" }
16
19
  create_middleware_stack("ussd")
17
20
  end
18
21
 
19
22
  def configure_middleware_stack(builder)
20
- builder.use FlowChat::Session::Middleware
23
+ FlowChat.logger.debug { "Ussd::Processor: Configuring USSD middleware stack" }
24
+
21
25
  builder.use FlowChat::Ussd::Middleware::Pagination
26
+ FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::Pagination" }
27
+
22
28
  builder.use middleware
29
+ FlowChat.logger.debug { "Ussd::Processor: Added custom middleware" }
30
+
31
+ builder.use FlowChat::Ussd::Middleware::ChoiceMapper
32
+ FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::ChoiceMapper" }
33
+
23
34
  builder.use FlowChat::Ussd::Middleware::Executor
35
+ FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::Executor" }
24
36
  end
25
37
  end
26
38
  end
@@ -14,7 +14,7 @@ module FlowChat
14
14
  private
15
15
 
16
16
  def build_prompt
17
- parts = [prompt, build_media, build_choices].compact
17
+ parts = [build_media, prompt, build_choices].compact
18
18
  parts.join "\n\n"
19
19
  end
20
20
 
@@ -1,3 +1,3 @@
1
1
  module FlowChat
2
- VERSION = "0.6.1"
2
+ VERSION = "0.8.0"
3
3
  end