flow_chat 0.6.1 ā 0.7.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +84 -1229
- data/docs/configuration.md +337 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +306 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +9 -18
- data/examples/ussd_controller.rb +32 -38
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +48 -2
- data/lib/flow_chat/config.rb +5 -0
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +37 -4
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +6 -6
- data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +14 -0
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +120 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +8 -0
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +22 -11
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
data/examples/ussd_controller.rb
CHANGED
@@ -25,17 +25,17 @@ class WelcomeFlow < FlowChat::Flow
|
|
25
25
|
def main_page
|
26
26
|
# Welcome the user
|
27
27
|
name = app.screen(:name) do |prompt|
|
28
|
-
prompt.ask "Welcome
|
28
|
+
prompt.ask "Welcome! What's your name?",
|
29
29
|
transform: ->(input) { input.strip.titleize }
|
30
30
|
end
|
31
31
|
|
32
32
|
# Show main menu with numbered options (USSD style)
|
33
33
|
choice = app.screen(:main_menu) do |prompt|
|
34
|
-
prompt.select "Hi #{name}! Choose
|
34
|
+
prompt.select "Hi #{name}! Choose:", {
|
35
35
|
"1" => "Account Info",
|
36
36
|
"2" => "Make Payment",
|
37
37
|
"3" => "Get Balance",
|
38
|
-
"4" => "
|
38
|
+
"4" => "Support"
|
39
39
|
}
|
40
40
|
end
|
41
41
|
|
@@ -55,54 +55,55 @@ class WelcomeFlow < FlowChat::Flow
|
|
55
55
|
|
56
56
|
def show_account_info
|
57
57
|
info_choice = app.screen(:account_info) do |prompt|
|
58
|
-
prompt.select "Account
|
58
|
+
prompt.select "Account Info:", {
|
59
59
|
"1" => "Personal Details",
|
60
|
-
"2" => "
|
60
|
+
"2" => "Balance",
|
61
61
|
"3" => "Transaction History",
|
62
|
-
"0" => "
|
62
|
+
"0" => "Main Menu"
|
63
63
|
}
|
64
64
|
end
|
65
65
|
|
66
66
|
case info_choice
|
67
67
|
when "1"
|
68
|
-
app.say "Name: John Doe
|
68
|
+
app.say "Name: John Doe\nPhone: #{app.phone_number}\nStatus: Active"
|
69
69
|
when "2"
|
70
|
-
app.say "
|
70
|
+
app.say "Balance: $150.75\nCredit: $1,000.00"
|
71
71
|
when "3"
|
72
|
-
app.say "
|
72
|
+
app.say "Recent:\n+$50.00 Deposit\n-$25.50 Purchase\n-$15.00 Transfer"
|
73
73
|
when "0"
|
74
|
-
main_page
|
74
|
+
main_page
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
78
|
def make_payment
|
79
79
|
amount = app.screen(:payment_amount) do |prompt|
|
80
|
-
prompt.ask "Enter amount
|
81
|
-
|
82
|
-
|
83
|
-
return "
|
84
|
-
return "
|
80
|
+
prompt.ask "Enter amount:",
|
81
|
+
validate: ->(input) {
|
82
|
+
amt = input.to_f
|
83
|
+
return "Invalid amount" unless amt > 0
|
84
|
+
return "Max $500" unless amt <= 500
|
85
85
|
nil
|
86
|
-
}
|
86
|
+
},
|
87
|
+
transform: ->(input) { input.to_f }
|
87
88
|
end
|
88
89
|
|
89
90
|
recipient = app.screen(:payment_recipient) do |prompt|
|
90
|
-
prompt.ask "
|
91
|
+
prompt.ask "Recipient phone:",
|
91
92
|
validate: ->(input) {
|
92
|
-
return "
|
93
|
+
return "10 digits required" unless input.match?(/\A\d{10}\z/)
|
93
94
|
nil
|
94
95
|
}
|
95
96
|
end
|
96
97
|
|
97
98
|
# Confirmation screen
|
98
99
|
confirmed = app.screen(:payment_confirmation) do |prompt|
|
99
|
-
prompt.yes? "Pay $#{amount} to #{recipient}
|
100
|
+
prompt.yes? "Pay $#{amount} to #{recipient}?"
|
100
101
|
end
|
101
102
|
|
102
103
|
if confirmed
|
103
104
|
# Process payment (your business logic here)
|
104
105
|
transaction_id = process_payment(amount, recipient)
|
105
|
-
app.say "Payment successful
|
106
|
+
app.say "Payment successful!\nID: #{transaction_id}\nAmount: $#{amount}"
|
106
107
|
else
|
107
108
|
app.say "Payment cancelled"
|
108
109
|
end
|
@@ -111,16 +112,14 @@ class WelcomeFlow < FlowChat::Flow
|
|
111
112
|
def get_balance
|
112
113
|
# Simulate balance check
|
113
114
|
balance = check_account_balance(app.phone_number)
|
114
|
-
app.say "
|
115
|
+
app.say "Balance\n\nAvailable: $#{balance[:available]}\nPending: $#{balance[:pending]}"
|
115
116
|
end
|
116
117
|
|
117
118
|
def customer_support
|
118
119
|
support_choice = app.screen(:support_menu) do |prompt|
|
119
|
-
prompt.select "
|
120
|
-
"1" => "Report
|
121
|
-
"2" => "
|
122
|
-
"3" => "Technical Support",
|
123
|
-
"4" => "Speak to Agent",
|
120
|
+
prompt.select "Support:", {
|
121
|
+
"1" => "Report Issue",
|
122
|
+
"2" => "Contact Info",
|
124
123
|
"0" => "Main Menu"
|
125
124
|
}
|
126
125
|
end
|
@@ -129,11 +128,7 @@ class WelcomeFlow < FlowChat::Flow
|
|
129
128
|
when "1"
|
130
129
|
report_issue
|
131
130
|
when "2"
|
132
|
-
app.say "
|
133
|
-
when "3"
|
134
|
-
app.say "Technical Support:\\nCall: 123-456-7891\\nEmail: tech@company.com\\n24/7 Support Available"
|
135
|
-
when "4"
|
136
|
-
app.say "Connecting you to an agent...\\nPlease call 123-456-7890\\nOr visit our nearest branch"
|
131
|
+
app.say "Support:\nCall: 123-456-7890\nEmail: support@company.com\nHours: 9AM-5PM"
|
137
132
|
when "0"
|
138
133
|
main_page
|
139
134
|
end
|
@@ -141,7 +136,7 @@ class WelcomeFlow < FlowChat::Flow
|
|
141
136
|
|
142
137
|
def report_issue
|
143
138
|
issue_type = app.screen(:issue_type) do |prompt|
|
144
|
-
prompt.select "
|
139
|
+
prompt.select "Issue type:", {
|
145
140
|
"1" => "Payment Problem",
|
146
141
|
"2" => "Account Access",
|
147
142
|
"3" => "Service Error",
|
@@ -150,9 +145,9 @@ class WelcomeFlow < FlowChat::Flow
|
|
150
145
|
end
|
151
146
|
|
152
147
|
description = app.screen(:issue_description) do |prompt|
|
153
|
-
prompt.ask "
|
148
|
+
prompt.ask "Describe issue:",
|
154
149
|
validate: ->(input) {
|
155
|
-
return "
|
150
|
+
return "Min 10 characters" if input.length < 10
|
156
151
|
nil
|
157
152
|
}
|
158
153
|
end
|
@@ -160,7 +155,7 @@ class WelcomeFlow < FlowChat::Flow
|
|
160
155
|
# Save the issue (your business logic here)
|
161
156
|
ticket_id = create_support_ticket(issue_type, description, app.phone_number)
|
162
157
|
|
163
|
-
app.say "Issue reported
|
158
|
+
app.say "Issue reported!\n\nTicket: #{ticket_id}\nWe'll contact you within 24hrs"
|
164
159
|
end
|
165
160
|
|
166
161
|
# Helper methods (implement your business logic)
|
@@ -175,14 +170,13 @@ class WelcomeFlow < FlowChat::Flow
|
|
175
170
|
# Your balance checking logic here
|
176
171
|
{
|
177
172
|
available: "150.75",
|
178
|
-
pending: "25.00"
|
179
|
-
total: "175.75"
|
173
|
+
pending: "25.00"
|
180
174
|
}
|
181
175
|
end
|
182
176
|
|
183
177
|
def create_support_ticket(issue_type, description, phone_number)
|
184
178
|
# Your ticket creation logic here
|
185
|
-
Rails.logger.info "
|
179
|
+
Rails.logger.info "Ticket: #{issue_type} - #{description} from #{phone_number}"
|
186
180
|
"TICKET#{rand(10000..99999)}"
|
187
181
|
end
|
188
182
|
end
|
@@ -1,82 +1,38 @@
|
|
1
1
|
# Example WhatsApp Controller
|
2
2
|
# Add this to your Rails application as app/controllers/whatsapp_controller.rb
|
3
3
|
|
4
|
+
# Basic WhatsApp controller using Rails credentials
|
4
5
|
class WhatsappController < ApplicationController
|
5
6
|
skip_forgery_protection
|
6
7
|
|
7
8
|
def webhook
|
8
|
-
# Enable simulator mode for local endpoint testing in development
|
9
9
|
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
|
10
10
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
11
|
-
# Use cache-based session store for longer WhatsApp conversations
|
12
11
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
13
12
|
end
|
14
13
|
|
15
14
|
processor.run WelcomeFlow, :main_page
|
16
|
-
rescue FlowChat::Whatsapp::ConfigurationError => e
|
17
|
-
Rails.logger.error "WhatsApp configuration error: #{e.message}"
|
18
|
-
head :internal_server_error
|
19
15
|
rescue => e
|
20
|
-
Rails.logger.error "
|
16
|
+
Rails.logger.error "Error processing WhatsApp webhook: #{e.message}"
|
21
17
|
head :internal_server_error
|
22
18
|
end
|
23
19
|
end
|
24
20
|
|
25
|
-
#
|
21
|
+
# Controller with custom configuration
|
26
22
|
class CustomWhatsappController < ApplicationController
|
27
23
|
skip_forgery_protection
|
28
24
|
|
29
25
|
def webhook
|
30
|
-
# Create custom WhatsApp configuration for this endpoint
|
31
|
-
custom_config = FlowChat::Whatsapp::Configuration.new
|
32
|
-
custom_config.access_token = ENV["MY_WHATSAPP_ACCESS_TOKEN"]
|
33
|
-
custom_config.phone_number_id = ENV["MY_WHATSAPP_PHONE_NUMBER_ID"]
|
34
|
-
custom_config.verify_token = ENV["MY_WHATSAPP_VERIFY_TOKEN"]
|
35
|
-
custom_config.app_id = ENV["MY_WHATSAPP_APP_ID"]
|
36
|
-
custom_config.app_secret = ENV["MY_WHATSAPP_APP_SECRET"]
|
37
|
-
custom_config.business_account_id = ENV["MY_WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
38
|
-
|
39
|
-
# Security configuration
|
40
|
-
custom_config.skip_signature_validation = !Rails.env.production? # Only skip in non-production
|
41
|
-
|
42
|
-
# Enable simulator for local endpoint testing in non-production environments
|
43
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
|
44
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_config
|
45
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
46
|
-
end
|
47
|
-
|
48
|
-
processor.run WelcomeFlow, :main_page
|
49
|
-
rescue FlowChat::Whatsapp::ConfigurationError => e
|
50
|
-
Rails.logger.error "WhatsApp configuration error: #{e.message}"
|
51
|
-
head :internal_server_error
|
52
|
-
rescue => e
|
53
|
-
Rails.logger.error "Unexpected error processing WhatsApp webhook: #{e.message}"
|
54
|
-
head :internal_server_error
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# Example with Environment-Specific Security
|
59
|
-
class EnvironmentAwareWhatsappController < ApplicationController
|
60
|
-
skip_forgery_protection
|
61
|
-
|
62
|
-
def webhook
|
63
|
-
# Configure security based on environment
|
64
26
|
custom_config = build_whatsapp_config
|
65
|
-
|
66
|
-
# Enable simulator for local endpoint testing in development/staging
|
67
|
-
enable_simulator = Rails.env.development? || Rails.env.staging?
|
68
27
|
|
69
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator:
|
28
|
+
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
|
70
29
|
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_config
|
71
30
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
72
31
|
end
|
73
32
|
|
74
33
|
processor.run WelcomeFlow, :main_page
|
75
|
-
rescue FlowChat::Whatsapp::ConfigurationError => e
|
76
|
-
Rails.logger.error "WhatsApp configuration error: #{e.message}"
|
77
|
-
head :internal_server_error
|
78
34
|
rescue => e
|
79
|
-
Rails.logger.error "
|
35
|
+
Rails.logger.error "Error processing WhatsApp webhook: #{e.message}"
|
80
36
|
head :internal_server_error
|
81
37
|
end
|
82
38
|
|
@@ -84,63 +40,41 @@ class EnvironmentAwareWhatsappController < ApplicationController
|
|
84
40
|
|
85
41
|
def build_whatsapp_config
|
86
42
|
config = FlowChat::Whatsapp::Configuration.new
|
87
|
-
|
43
|
+
|
88
44
|
case Rails.env
|
89
|
-
when
|
90
|
-
# Development: More relaxed security for easier testing
|
45
|
+
when "development", "test"
|
91
46
|
config.access_token = ENV["WHATSAPP_ACCESS_TOKEN"]
|
92
47
|
config.phone_number_id = ENV["WHATSAPP_PHONE_NUMBER_ID"]
|
93
48
|
config.verify_token = ENV["WHATSAPP_VERIFY_TOKEN"]
|
94
|
-
config.
|
95
|
-
config.
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
when 'test'
|
100
|
-
# Test: Use test credentials
|
101
|
-
config.access_token = "test_token"
|
102
|
-
config.phone_number_id = "test_phone_id"
|
103
|
-
config.verify_token = "test_verify_token"
|
104
|
-
config.app_id = "test_app_id"
|
105
|
-
config.app_secret = "test_app_secret"
|
106
|
-
config.business_account_id = "test_business_id"
|
107
|
-
config.skip_signature_validation = true # Skip validation in tests
|
108
|
-
|
109
|
-
when 'staging', 'production'
|
110
|
-
# Production: Full security enabled
|
49
|
+
config.app_secret = ENV["WHATSAPP_APP_SECRET"]
|
50
|
+
config.skip_signature_validation = true # Skip for easier development
|
51
|
+
|
52
|
+
when "staging", "production"
|
111
53
|
config.access_token = ENV["WHATSAPP_ACCESS_TOKEN"]
|
112
54
|
config.phone_number_id = ENV["WHATSAPP_PHONE_NUMBER_ID"]
|
113
55
|
config.verify_token = ENV["WHATSAPP_VERIFY_TOKEN"]
|
114
|
-
config.
|
115
|
-
config.app_secret = ENV["WHATSAPP_APP_SECRET"] # Required for security
|
116
|
-
config.business_account_id = ENV["WHATSAPP_BUSINESS_ACCOUNT_ID"]
|
56
|
+
config.app_secret = ENV["WHATSAPP_APP_SECRET"]
|
117
57
|
config.skip_signature_validation = false # Always validate in production
|
118
|
-
|
119
|
-
# Ensure required security configuration is present
|
58
|
+
|
120
59
|
if config.app_secret.blank?
|
121
|
-
raise
|
122
|
-
"WHATSAPP_APP_SECRET is required for webhook signature validation in #{Rails.env}"
|
60
|
+
raise "WHATSAPP_APP_SECRET required for signature validation in #{Rails.env}"
|
123
61
|
end
|
124
62
|
end
|
125
|
-
|
63
|
+
|
126
64
|
config
|
127
65
|
end
|
128
66
|
end
|
129
67
|
|
130
|
-
# Example
|
131
|
-
# Add this to your Rails application as app/flow_chat/welcome_flow.rb
|
132
|
-
|
68
|
+
# Example flow for WhatsApp
|
133
69
|
class WelcomeFlow < FlowChat::Flow
|
134
70
|
def main_page
|
135
|
-
# Welcome the user
|
136
71
|
name = app.screen(:name) do |prompt|
|
137
|
-
prompt.ask "Hello!
|
72
|
+
prompt.ask "Hello! What's your name?",
|
138
73
|
transform: ->(input) { input.strip.titleize }
|
139
74
|
end
|
140
75
|
|
141
|
-
# Show main menu
|
142
76
|
choice = app.screen(:main_menu) do |prompt|
|
143
|
-
prompt.select "Hi #{name}!
|
77
|
+
prompt.select "Hi #{name}! How can I help?", {
|
144
78
|
"info" => "š Get Information",
|
145
79
|
"support" => "š Contact Support",
|
146
80
|
"feedback" => "š¬ Give Feedback"
|
@@ -149,7 +83,7 @@ class WelcomeFlow < FlowChat::Flow
|
|
149
83
|
|
150
84
|
case choice
|
151
85
|
when "info"
|
152
|
-
|
86
|
+
show_info
|
153
87
|
when "support"
|
154
88
|
contact_support
|
155
89
|
when "feedback"
|
@@ -159,69 +93,42 @@ class WelcomeFlow < FlowChat::Flow
|
|
159
93
|
|
160
94
|
private
|
161
95
|
|
162
|
-
def
|
163
|
-
|
164
|
-
prompt.select "What information do you need?", {
|
165
|
-
"hours" => "š Business Hours",
|
166
|
-
"location" => "š Our Location",
|
167
|
-
"services" => "š Our Services"
|
168
|
-
}
|
169
|
-
end
|
170
|
-
|
171
|
-
case info_choice
|
172
|
-
when "hours"
|
173
|
-
app.say "We're open Monday-Friday 9AM-6PM, Saturday 9AM-2PM. Closed Sundays."
|
174
|
-
when "location"
|
175
|
-
app.say "š We're located at 123 Main Street, City, State 12345"
|
176
|
-
when "services"
|
177
|
-
app.say "Here are our main services:\\n\\nš Web Development - Custom websites and applications\\nš± Mobile Apps - iOS and Android development\\nš§ Consulting - Technical consulting services"
|
178
|
-
end
|
96
|
+
def show_info
|
97
|
+
app.say "š Located at 123 Main Street\nš Hours: Mon-Fri 9AM-6PM\nš Call: (555) 123-4567"
|
179
98
|
end
|
180
99
|
|
181
100
|
def contact_support
|
182
|
-
|
183
|
-
|
184
|
-
prompt.select "How would you like to contact support?", {
|
101
|
+
method = app.screen(:contact_method) do |prompt|
|
102
|
+
prompt.select "How would you like to contact us?", {
|
185
103
|
"call" => "š Call Us",
|
186
|
-
"email" => "š§ Email Us"
|
187
|
-
"chat" => "š¬ Live Chat"
|
104
|
+
"email" => "š§ Email Us"
|
188
105
|
}
|
189
106
|
end
|
190
107
|
|
191
|
-
case
|
108
|
+
case method
|
192
109
|
when "call"
|
193
|
-
app.say "š
|
110
|
+
app.say "š Call us at (555) 123-4567"
|
194
111
|
when "email"
|
195
|
-
app.say "š§
|
196
|
-
when "chat"
|
197
|
-
app.say "š¬ Our live chat is available on our website: www.example.com"
|
112
|
+
app.say "š§ Email us at support@example.com"
|
198
113
|
end
|
199
114
|
end
|
200
115
|
|
201
116
|
def collect_feedback
|
202
117
|
rating = app.screen(:rating) do |prompt|
|
203
|
-
prompt.select "
|
204
|
-
"5" => "āāāāā Excellent",
|
205
|
-
"4" => "āāāā Good",
|
206
|
-
"3" => "āāā Average",
|
207
|
-
"2" => "āā Poor",
|
208
|
-
"1" => "ā Very Poor"
|
209
|
-
}
|
118
|
+
prompt.select "Rate our service:", ["ā", "āā", "āāā", "āāāā", "āāāāā"]
|
210
119
|
end
|
211
120
|
|
212
121
|
feedback = app.screen(:feedback_text) do |prompt|
|
213
|
-
prompt.ask "
|
122
|
+
prompt.ask "Any additional comments?"
|
214
123
|
end
|
215
124
|
|
216
|
-
# Save feedback (implement your logic here)
|
217
125
|
save_feedback(app.phone_number, rating, feedback)
|
218
|
-
|
219
|
-
app.say "Thank you for your feedback! We really appreciate it. š"
|
126
|
+
app.say "Thank you for your feedback! š"
|
220
127
|
end
|
221
128
|
|
222
129
|
def save_feedback(phone, rating, feedback)
|
223
|
-
|
224
|
-
|
130
|
+
Rails.logger.info "Feedback from #{phone}: #{rating} - #{feedback}"
|
131
|
+
# Add your feedback saving logic here
|
225
132
|
end
|
226
133
|
end
|
227
134
|
|