flow_chat 0.8.1 → 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 +154 -0
  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/simulator_controller.rb +21 -1
  31. data/examples/ussd_controller.rb +10 -10
  32. data/examples/whatsapp_controller.rb +2 -2
  33. data/flow_chat.gemspec +4 -0
  34. data/lib/flow_chat/{base_app.rb → app.rb} +28 -9
  35. data/lib/flow_chat/async_job.rb +176 -0
  36. data/lib/flow_chat/config.rb +23 -28
  37. data/lib/flow_chat/{base_executor.rb → executor.rb} +7 -12
  38. data/lib/flow_chat/factory.rb +94 -0
  39. data/lib/flow_chat/gateway_async_support.rb +106 -0
  40. data/lib/flow_chat/generic_async_job.rb +30 -0
  41. data/lib/flow_chat/http/gateway/simple.rb +125 -0
  42. data/lib/flow_chat/http/renderer.rb +41 -0
  43. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  44. data/lib/flow_chat/instrumentation.rb +25 -0
  45. data/lib/flow_chat/intercom/client.rb +155 -0
  46. data/lib/flow_chat/intercom/configuration.rb +149 -0
  47. data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
  48. data/lib/flow_chat/intercom/renderer.rb +71 -0
  49. data/lib/flow_chat/phone_number_util.rb +49 -0
  50. data/lib/flow_chat/processor.rb +188 -0
  51. data/lib/flow_chat/renderers/markdown_support.rb +58 -0
  52. data/lib/flow_chat/session/cache_session_store.rb +1 -17
  53. data/lib/flow_chat/session/middleware.rb +43 -26
  54. data/lib/flow_chat/simulator/controller.rb +17 -5
  55. data/lib/flow_chat/simulator/views/simulator.html.erb +220 -8
  56. data/lib/flow_chat/telegram/client.rb +240 -0
  57. data/lib/flow_chat/telegram/configuration.rb +118 -0
  58. data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
  59. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
  60. data/lib/flow_chat/telegram/renderer.rb +125 -0
  61. data/lib/flow_chat/telegram.rb +7 -0
  62. data/lib/flow_chat/ussd/gateway/nalo.rb +27 -11
  63. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  64. data/lib/flow_chat/ussd/renderer.rb +1 -1
  65. data/lib/flow_chat/version.rb +1 -1
  66. data/lib/flow_chat/whatsapp/client.rb +144 -13
  67. data/lib/flow_chat/whatsapp/configuration.rb +1 -1
  68. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +144 -106
  69. data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
  70. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
  71. data/lib/flow_chat/whatsapp/renderer.rb +145 -11
  72. data/lib/flow_chat.rb +11 -1
  73. data/lib/tasks/release.rake +165 -0
  74. metadata +88 -21
  75. data/docs/flows.md +0 -320
  76. data/docs/images/simulator.png +0 -0
  77. data/docs/media.md +0 -153
  78. data/docs/sessions.md +0 -433
  79. data/docs/ussd-setup.md +0 -322
  80. data/docs/whatsapp-setup.md +0 -162
  81. data/examples/whatsapp_message_job.rb +0 -113
  82. data/lib/flow_chat/base_processor.rb +0 -145
  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 -98
  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
@@ -1,145 +0,0 @@
1
- require "middleware"
2
-
3
- module FlowChat
4
- class BaseProcessor
5
- include FlowChat::Instrumentation
6
-
7
- attr_reader :middleware
8
-
9
- def initialize(controller, enable_simulator: nil)
10
- FlowChat.logger.debug { "BaseProcessor: Initializing processor for controller #{controller.class.name}" }
11
-
12
- @context = FlowChat::Context.new
13
- @context["controller"] = controller
14
- @context["enable_simulator"] = enable_simulator.nil? ? (defined?(Rails) && Rails.env.local?) : enable_simulator
15
- @middleware = ::Middleware::Builder.new(name: middleware_name)
16
- @session_options = FlowChat::Config.session
17
-
18
- FlowChat.logger.debug { "BaseProcessor: Simulator mode #{@context["enable_simulator"] ? "enabled" : "disabled"}" }
19
-
20
- yield self if block_given?
21
-
22
- FlowChat.logger.debug { "BaseProcessor: Initialized #{self.class.name} successfully" }
23
- end
24
-
25
- def use_gateway(gateway_class, *args)
26
- FlowChat.logger.debug { "BaseProcessor: Configuring gateway #{gateway_class.name} with args: #{args.inspect}" }
27
- @gateway_class = gateway_class
28
- @gateway_args = args
29
- self
30
- end
31
-
32
- def use_session_store(session_store)
33
- raise "Session store must be a class" unless session_store.is_a?(Class)
34
- FlowChat.logger.debug { "BaseProcessor: Configuring session store #{session_store.name}" }
35
- @context["session.store"] = session_store
36
- self
37
- end
38
-
39
- def use_session_config(boundaries: nil, hash_phone_numbers: nil, identifier: nil)
40
- FlowChat.logger.debug { "BaseProcessor: Configuring session config: boundaries=#{boundaries.inspect}, hash_phone_numbers=#{hash_phone_numbers}, identifier=#{identifier}" }
41
-
42
- # Update the session options directly
43
- @session_options = @session_options.dup
44
- @session_options.boundaries = Array(boundaries) if boundaries
45
- @session_options.hash_phone_numbers = hash_phone_numbers if hash_phone_numbers
46
- @session_options.identifier = identifier if identifier
47
-
48
- self
49
- end
50
-
51
- def use_middleware(middleware)
52
- raise "Middleware must be a class" unless middleware.is_a?(Class)
53
- FlowChat.logger.debug { "BaseProcessor: Adding middleware #{middleware.name}" }
54
- @middleware.use middleware
55
- self
56
- end
57
-
58
- def use_cross_platform_sessions
59
- FlowChat.logger.debug { "BaseProcessor: Enabling cross-platform sessions via session configuration" }
60
- use_session_config(
61
- boundaries: [:flow]
62
- )
63
- self
64
- end
65
-
66
- def use_url_isolation
67
- FlowChat.logger.debug { "BaseProcessor: Enabling URL-based session isolation" }
68
- current_boundaries = @session_options.boundaries.dup
69
- current_boundaries << :url unless current_boundaries.include?(:url)
70
- use_session_config(boundaries: current_boundaries)
71
- end
72
-
73
- def run(flow_class, action)
74
- # Instrument flow execution (this will log via LogSubscriber)
75
- instrument(Events::FLOW_EXECUTION_START, {
76
- flow_name: flow_class.name.underscore,
77
- action: action.to_s,
78
- processor_type: self.class.name
79
- })
80
-
81
- @context["flow.name"] = flow_class.name.underscore
82
- @context["flow.class"] = flow_class
83
- @context["flow.action"] = action
84
-
85
- FlowChat.logger.debug { "BaseProcessor: Context prepared for flow #{flow_class.name}" }
86
-
87
- stack = build_middleware_stack
88
- yield stack if block_given?
89
-
90
- FlowChat.logger.debug { "BaseProcessor: Executing middleware stack for #{flow_class.name}##{action}" }
91
-
92
- # Instrument flow execution with timing (this will log completion via LogSubscriber)
93
- instrument(Events::FLOW_EXECUTION_END, {
94
- flow_name: flow_class.name.underscore,
95
- action: action.to_s,
96
- processor_type: self.class.name
97
- }) do
98
- stack.call(@context)
99
- end
100
- rescue => error
101
- FlowChat.logger.error { "BaseProcessor: Flow execution failed - #{flow_class.name}##{action}, Error: #{error.class.name}: #{error.message}" }
102
- FlowChat.logger.debug { "BaseProcessor: Stack trace: #{error.backtrace.join("\n")}" }
103
-
104
- # Instrument flow execution error (this will log error via LogSubscriber)
105
- instrument(Events::FLOW_EXECUTION_ERROR, {
106
- flow_name: flow_class.name.underscore,
107
- action: action.to_s,
108
- processor_type: self.class.name,
109
- error_class: error.class.name,
110
- error_message: error.message,
111
- backtrace: error.backtrace&.first(10)
112
- })
113
-
114
- raise
115
- end
116
-
117
- protected
118
-
119
- # Subclasses should override these methods
120
- def middleware_name
121
- raise NotImplementedError, "Subclasses must implement middleware_name"
122
- end
123
-
124
- def build_middleware_stack
125
- raise NotImplementedError, "Subclasses must implement build_middleware_stack"
126
- end
127
-
128
- # Helper method for building stacks
129
- def create_middleware_stack(name)
130
- raise ArgumentError, "Gateway is required. Call use_gateway(gateway_class, *args) before running." unless @gateway_class
131
-
132
- ::Middleware::Builder.new(name: name) do |b|
133
- b.use @gateway_class, *@gateway_args
134
- b.use FlowChat::Session::Middleware, @session_options
135
- configure_middleware_stack(b)
136
- end.inject_logger(FlowChat.logger)
137
- end
138
-
139
- def configure_middleware_stack(builder)
140
- raise NotImplementedError, "Subclasses must implement configure_middleware_stack"
141
- end
142
-
143
- attr_reader :context
144
- end
145
- end
@@ -1,68 +0,0 @@
1
- module FlowChat
2
- module Session
3
- class RailsSessionStore
4
- include FlowChat::Instrumentation
5
-
6
- # Make context available for instrumentation enrichment
7
- attr_reader :context
8
-
9
- def initialize(context)
10
- @context = context
11
- @session_id = context["session.id"]
12
- @session_store = context.controller.session
13
- @session_data = (session_store[session_id] || {}).with_indifferent_access
14
-
15
- FlowChat.logger.debug { "RailsSessionStore: Initialized Rails session store for session #{session_id}" }
16
- FlowChat.logger.debug { "RailsSessionStore: Loaded session data with #{session_data.keys.size} keys" }
17
- end
18
-
19
- def get(key)
20
- value = session_data[key]
21
-
22
- # Use instrumentation for data get
23
- instrument(Events::SESSION_DATA_GET, {
24
- session_id: session_id,
25
- key: key.to_s,
26
- value: value
27
- })
28
-
29
- value
30
- end
31
-
32
- def set(key, value)
33
- FlowChat.logger.debug { "RailsSessionStore: Setting key '#{key}' = #{value.inspect} in session #{session_id}" }
34
-
35
- session_data[key] = value
36
- session_store[session_id] = session_data
37
-
38
- # Use instrumentation for data set
39
- instrument(Events::SESSION_DATA_SET, {
40
- session_id: session_id,
41
- key: key.to_s
42
- })
43
-
44
- FlowChat.logger.debug { "RailsSessionStore: Session data saved to Rails session store" }
45
- value
46
- end
47
-
48
- def delete(key)
49
- FlowChat.logger.debug { "RailsSessionStore: Deleting key '#{key}' from session #{session_id}" }
50
- set key, nil
51
- end
52
-
53
- def destroy
54
- # Use instrumentation for session destruction
55
- instrument(Events::SESSION_DESTROYED, {
56
- session_id: session_id,
57
- gateway: "rails" # Rails doesn't have a specific gateway context
58
- })
59
-
60
- session_store[session_id] = nil
61
- end
62
-
63
- private
64
-
65
- attr_reader :session_id, :session_store, :session_data
66
- end
67
- end
68
- end
@@ -1,6 +0,0 @@
1
- module FlowChat
2
- module Ussd
3
- class App < FlowChat::BaseApp
4
- end
5
- end
6
- end
@@ -1,98 +0,0 @@
1
- require "phonelib"
2
-
3
- module FlowChat
4
- module Ussd
5
- module Gateway
6
- class Nsano
7
- include FlowChat::Instrumentation
8
-
9
- attr_reader :context
10
-
11
- def initialize(app)
12
- @app = app
13
- end
14
-
15
- def call(context)
16
- @context = context
17
- controller = context["controller"]
18
- controller.request
19
-
20
- # Add timestamp for all requests
21
- context["request.timestamp"] = Time.current.iso8601
22
-
23
- # Set a basic message_id (can be enhanced based on actual Nsano implementation)
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
- })
54
-
55
- # input = context["rack.input"].read
56
- # context["rack.input"].rewind
57
- # if input.present?
58
- # params = JSON.parse input
59
- # if params["network"].present? && params["UserSessionID"].present?
60
- # request_id = "nsano::request_id::#{params["UserSessionID"]}"
61
- # context["ussd.request"] = {
62
- # gateway: :nsano,
63
- # network: params["network"].to_sym,
64
- # msisdn: Phonelib.parse(params["msisdn"]).e164,
65
- # type: Config.cache&.read(request_id).present? ? :response : :initial,
66
- # input: params["msg"].presence,
67
- # network: params["network"]
68
- # }
69
- # end
70
- # end
71
-
72
- # status, headers, response = @app.call(context)
73
-
74
- # if context["ussd.response"].present? && context["ussd.request"][:gateway] == :nsano
75
- # if context["ussd.response"][:type] == :terminal
76
- # Config.cache&.write(request_id, nil)
77
- # else
78
- # Config.cache&.write(request_id, 1)
79
- # end
80
-
81
- # status = 200
82
- # response =
83
- # {
84
- # USSDResp: {
85
- # action: (context["ussd.response"][:type] == :terminal) ? :prompt : :input,
86
- # menus: "",
87
- # title: context["ussd.response"][:body]
88
- # }
89
- # }.to_json
90
- # headers = headers.merge({"Content-Type" => "application/json", "Content-Length" => response.bytesize.to_s})
91
- # response = [response]
92
- # end
93
- # [status, headers, response]
94
- end
95
- end
96
- end
97
- end
98
- end
@@ -1,24 +0,0 @@
1
- require_relative "../../base_executor"
2
-
3
- module FlowChat
4
- module Ussd
5
- module Middleware
6
- class Executor < FlowChat::BaseExecutor
7
- protected
8
-
9
- def platform_name
10
- "USSD"
11
- end
12
-
13
- def log_prefix
14
- "Ussd::Executor"
15
- end
16
-
17
- def build_platform_app(context)
18
- FlowChat.logger.debug { "#{log_prefix}: Building USSD app instance" }
19
- FlowChat::Ussd::App.new(context)
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,39 +0,0 @@
1
- module FlowChat
2
- module Ussd
3
- class Processor < FlowChat::BaseProcessor
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
- )
9
- end
10
-
11
- protected
12
-
13
- def middleware_name
14
- "ussd.middleware"
15
- end
16
-
17
- def build_middleware_stack
18
- FlowChat.logger.debug { "Ussd::Processor: Building USSD middleware stack" }
19
- create_middleware_stack("ussd")
20
- end
21
-
22
- def configure_middleware_stack(builder)
23
- FlowChat.logger.debug { "Ussd::Processor: Configuring USSD middleware stack" }
24
-
25
- builder.use FlowChat::Ussd::Middleware::Pagination
26
- FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::Pagination" }
27
-
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
-
34
- builder.use FlowChat::Ussd::Middleware::Executor
35
- FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::Executor" }
36
- end
37
- end
38
- end
39
- end
@@ -1,29 +0,0 @@
1
- module FlowChat
2
- module Whatsapp
3
- class App < FlowChat::BaseApp
4
- def contact_name
5
- context["request.contact_name"]
6
- end
7
-
8
- def location
9
- context["request.location"]
10
- end
11
-
12
- def media
13
- context["request.media"]
14
- end
15
-
16
- protected
17
-
18
- # WhatsApp has special startup logic and supports media
19
- def prepare_user_input
20
- user_input = input
21
- if session.get("$started_at$").nil?
22
- session.set("$started_at$", Time.current.iso8601)
23
- user_input = nil
24
- end
25
- user_input
26
- end
27
- end
28
- end
29
- end
@@ -1,24 +0,0 @@
1
- require_relative "../../base_executor"
2
-
3
- module FlowChat
4
- module Whatsapp
5
- module Middleware
6
- class Executor < FlowChat::BaseExecutor
7
- protected
8
-
9
- def platform_name
10
- "WhatsApp"
11
- end
12
-
13
- def log_prefix
14
- "Whatsapp::Executor"
15
- end
16
-
17
- def build_platform_app(context)
18
- FlowChat.logger.debug { "#{log_prefix}: Building WhatsApp app instance" }
19
- FlowChat::Whatsapp::App.new(context)
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,32 +0,0 @@
1
- module FlowChat
2
- module Whatsapp
3
- class Processor < FlowChat::BaseProcessor
4
- def use_whatsapp_config(config)
5
- FlowChat.logger.debug { "Whatsapp::Processor: Configuring WhatsApp config: #{config.class.name}" }
6
- @whatsapp_config = config
7
- self
8
- end
9
-
10
- protected
11
-
12
- def middleware_name
13
- "whatsapp.middleware"
14
- end
15
-
16
- def build_middleware_stack
17
- FlowChat.logger.debug { "Whatsapp::Processor: Building WhatsApp middleware stack" }
18
- create_middleware_stack("whatsapp")
19
- end
20
-
21
- def configure_middleware_stack(builder)
22
- FlowChat.logger.debug { "Whatsapp::Processor: Configuring WhatsApp middleware stack" }
23
-
24
- builder.use middleware
25
- FlowChat.logger.debug { "Whatsapp::Processor: Added custom middleware" }
26
-
27
- builder.use FlowChat::Whatsapp::Middleware::Executor
28
- FlowChat.logger.debug { "Whatsapp::Processor: Added Whatsapp::Middleware::Executor" }
29
- end
30
- end
31
- end
32
- end
@@ -1,79 +0,0 @@
1
- module FlowChat
2
- module Whatsapp
3
- # Module to be included in background jobs for WhatsApp response delivery
4
- # Only handles sending responses, not processing flows
5
- module SendJobSupport
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- # Set up job configuration
10
- queue_as :default
11
- retry_on StandardError, wait: :exponentially_longer, attempts: 3
12
- end
13
-
14
- # Main job execution method for sending responses
15
- def perform_whatsapp_send(send_data)
16
- config = resolve_whatsapp_config(send_data)
17
- client = FlowChat::Whatsapp::Client.new(config)
18
-
19
- result = client.send_message(send_data[:msisdn], send_data[:response])
20
-
21
- if result
22
- Rails.logger.info "WhatsApp message sent successfully: #{result["messages"]&.first&.dig("id")}"
23
- on_whatsapp_send_success(send_data, result)
24
- else
25
- Rails.logger.error "Failed to send WhatsApp message to #{send_data[:msisdn]}"
26
- raise "WhatsApp API call failed"
27
- end
28
- rescue => e
29
- on_whatsapp_send_error(e, send_data)
30
- handle_whatsapp_send_error(e, send_data, config)
31
- end
32
-
33
- private
34
-
35
- # Resolve WhatsApp configuration by name or fallback
36
- def resolve_whatsapp_config(send_data)
37
- # Try to resolve by name first (preferred method)
38
- if send_data[:config_name] && FlowChat::Whatsapp::Configuration.exists?(send_data[:config_name])
39
- return FlowChat::Whatsapp::Configuration.get(send_data[:config_name])
40
- end
41
-
42
- # Final fallback to default configuration
43
- FlowChat::Whatsapp::Configuration.from_credentials
44
- end
45
-
46
- # Handle errors with user notification
47
- def handle_whatsapp_send_error(error, send_data, config = nil)
48
- Rails.logger.error "WhatsApp send job error: #{error.message}"
49
- Rails.logger.error error.backtrace&.join("\n") if error.backtrace
50
-
51
- # Try to send error message to user if we have config
52
- if config
53
- begin
54
- client = FlowChat::Whatsapp::Client.new(config)
55
- client.send_text(
56
- send_data[:msisdn],
57
- "⚠️ We're experiencing technical difficulties. Please try again in a few minutes."
58
- )
59
- rescue => send_error
60
- Rails.logger.error "Failed to send error message: #{send_error.message}"
61
- end
62
- end
63
-
64
- # Re-raise for job retry logic
65
- raise error
66
- end
67
-
68
- # Override this method in your job for custom behavior
69
- def on_whatsapp_send_success(send_data, result)
70
- # Optional callback for successful message sending
71
- end
72
-
73
- # Override this method in your job for custom error handling
74
- def on_whatsapp_send_error(error, send_data)
75
- # Optional callback for error handling
76
- end
77
- end
78
- end
79
- end