flow_chat 0.6.0 → 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 +7 -7
- data/lib/flow_chat/ussd/app.rb +1 -1
- 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/app.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 +128 -54
- 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
@@ -1,889 +0,0 @@
|
|
1
|
-
# FlowChat Comprehensive Demo: Restaurant Ordering System
|
2
|
-
# This flow demonstrates all major FlowChat features:
|
3
|
-
# - Cross-platform compatibility (USSD + WhatsApp)
|
4
|
-
# - Media support with graceful degradation
|
5
|
-
# - Input validation and transformation
|
6
|
-
# - Complex menu selection (arrays, hashes, large lists)
|
7
|
-
# - Session state management
|
8
|
-
# - Error handling and validation
|
9
|
-
# - Platform-specific features with fallbacks
|
10
|
-
# - Rich interactive elements
|
11
|
-
|
12
|
-
class DemoRestaurantFlow < FlowChat::Flow
|
13
|
-
def main_page
|
14
|
-
# Welcome with media (logo)
|
15
|
-
app.say "🍽️ Welcome to FlowChat Restaurant!",
|
16
|
-
media: {
|
17
|
-
type: :image,
|
18
|
-
url: "https://flowchat-demo.com/restaurant-logo.jpg"
|
19
|
-
}
|
20
|
-
|
21
|
-
# Check if returning customer
|
22
|
-
returning_customer = check_returning_customer
|
23
|
-
|
24
|
-
if returning_customer
|
25
|
-
name = app.session.get(:customer_name)
|
26
|
-
app.say "Welcome back, #{name}! 😊"
|
27
|
-
main_menu
|
28
|
-
else
|
29
|
-
customer_registration
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def check_returning_customer
|
36
|
-
# Check if we have customer data in session
|
37
|
-
app.session.get(:customer_name).present?
|
38
|
-
end
|
39
|
-
|
40
|
-
def customer_registration
|
41
|
-
app.say "Let's get you registered! 📝"
|
42
|
-
|
43
|
-
# Name with transformation
|
44
|
-
name = app.screen(:customer_name) do |prompt|
|
45
|
-
prompt.ask "What's your name?",
|
46
|
-
transform: ->(input) { input.strip.titleize },
|
47
|
-
validate: ->(input) {
|
48
|
-
return "Name must be at least 2 characters" if input.length < 2
|
49
|
-
return "Name can't be longer than 50 characters" if input.length > 50
|
50
|
-
return "Name can only contain letters and spaces" unless input.match?(/\A[a-zA-Z\s]+\z/)
|
51
|
-
nil
|
52
|
-
}
|
53
|
-
end
|
54
|
-
|
55
|
-
# Phone number with validation (international format)
|
56
|
-
phone = app.screen(:customer_phone) do |prompt|
|
57
|
-
prompt.ask "Enter your phone number (e.g., +1234567890):",
|
58
|
-
transform: ->(input) { input.strip.gsub(/[\s\-\(\)]/, '') },
|
59
|
-
validate: ->(input) {
|
60
|
-
return "Phone number must start with +" unless input.start_with?('+')
|
61
|
-
return "Phone number must be 8-15 digits after +" unless input[1..-1].match?(/\A\d{8,15}\z/)
|
62
|
-
nil
|
63
|
-
}
|
64
|
-
end
|
65
|
-
|
66
|
-
# Age with conversion and validation
|
67
|
-
age = app.screen(:customer_age) do |prompt|
|
68
|
-
prompt.ask "How old are you?",
|
69
|
-
convert: ->(input) { input.to_i },
|
70
|
-
validate: ->(age) {
|
71
|
-
return "You must be at least 13 to order" if age < 13
|
72
|
-
return "Age must be reasonable (under 120)" if age > 120
|
73
|
-
nil
|
74
|
-
}
|
75
|
-
end
|
76
|
-
|
77
|
-
# Dietary preferences with hash-based selection
|
78
|
-
dietary_preference = app.screen(:dietary_preference) do |prompt|
|
79
|
-
prompt.select "Any dietary preferences?", {
|
80
|
-
"none" => "No restrictions",
|
81
|
-
"vegetarian" => "Vegetarian",
|
82
|
-
"vegan" => "Vegan",
|
83
|
-
"gluten_free" => "Gluten-free",
|
84
|
-
"keto" => "Keto",
|
85
|
-
"halal" => "Halal"
|
86
|
-
}
|
87
|
-
end
|
88
|
-
|
89
|
-
# Store customer data
|
90
|
-
customer_data = {
|
91
|
-
name: name,
|
92
|
-
phone: phone,
|
93
|
-
age: age,
|
94
|
-
dietary_preference: dietary_preference,
|
95
|
-
registered_at: Time.current.iso8601
|
96
|
-
}
|
97
|
-
|
98
|
-
app.session.set(:customer_data, customer_data)
|
99
|
-
|
100
|
-
app.say "Thanks for registering, #{name}! 🎉",
|
101
|
-
media: {
|
102
|
-
type: :sticker,
|
103
|
-
url: "https://flowchat-demo.com/welcome-sticker.webp"
|
104
|
-
}
|
105
|
-
|
106
|
-
main_menu
|
107
|
-
end
|
108
|
-
|
109
|
-
def main_menu
|
110
|
-
choice = app.screen(:main_menu) do |prompt|
|
111
|
-
prompt.select "What would you like to do?", [
|
112
|
-
"Browse Menu",
|
113
|
-
"View Cart",
|
114
|
-
"Order History",
|
115
|
-
"Account Settings",
|
116
|
-
"Contact Support"
|
117
|
-
]
|
118
|
-
end
|
119
|
-
|
120
|
-
case choice
|
121
|
-
when "Browse Menu"
|
122
|
-
browse_menu
|
123
|
-
when "View Cart"
|
124
|
-
view_cart
|
125
|
-
when "Order History"
|
126
|
-
order_history
|
127
|
-
when "Account Settings"
|
128
|
-
account_settings
|
129
|
-
when "Contact Support"
|
130
|
-
contact_support
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def browse_menu
|
135
|
-
app.say "Here's our delicious menu! 📋",
|
136
|
-
media: {
|
137
|
-
type: :image,
|
138
|
-
url: "https://flowchat-demo.com/menu-hero.jpg"
|
139
|
-
}
|
140
|
-
|
141
|
-
category = app.screen(:menu_category) do |prompt|
|
142
|
-
prompt.select "Choose a category:", {
|
143
|
-
"appetizers" => "🥗 Appetizers",
|
144
|
-
"mains" => "🍽️ Main Courses",
|
145
|
-
"desserts" => "🍰 Desserts",
|
146
|
-
"beverages" => "🥤 Beverages",
|
147
|
-
"specials" => "⭐ Today's Specials"
|
148
|
-
}
|
149
|
-
end
|
150
|
-
|
151
|
-
show_category_items(category)
|
152
|
-
end
|
153
|
-
|
154
|
-
def show_category_items(category)
|
155
|
-
items = get_menu_items(category)
|
156
|
-
|
157
|
-
app.say "#{category.titleize} Menu:",
|
158
|
-
media: {
|
159
|
-
type: :image,
|
160
|
-
url: "https://flowchat-demo.com/categories/#{category}.jpg"
|
161
|
-
}
|
162
|
-
|
163
|
-
# Large list selection to test pagination on USSD
|
164
|
-
item_choice = app.screen("#{category}_selection".to_sym) do |prompt|
|
165
|
-
prompt.select "Choose an item:", items
|
166
|
-
end
|
167
|
-
|
168
|
-
show_item_details(category, item_choice)
|
169
|
-
end
|
170
|
-
|
171
|
-
def get_menu_items(category)
|
172
|
-
# Simulate menu items (large list to test pagination)
|
173
|
-
case category
|
174
|
-
when "appetizers"
|
175
|
-
{
|
176
|
-
"caesar_salad" => "Caesar Salad - $12",
|
177
|
-
"bruschetta" => "Bruschetta - $8",
|
178
|
-
"calamari" => "Fried Calamari - $14",
|
179
|
-
"wings" => "Buffalo Wings - $10",
|
180
|
-
"nachos" => "Loaded Nachos - $11",
|
181
|
-
"shrimp_cocktail" => "Shrimp Cocktail - $16",
|
182
|
-
"cheese_board" => "Artisan Cheese Board - $18",
|
183
|
-
"soup" => "Soup of the Day - $7"
|
184
|
-
}
|
185
|
-
when "mains"
|
186
|
-
# Large list to test USSD pagination
|
187
|
-
items = {}
|
188
|
-
dishes = [
|
189
|
-
"Grilled Salmon", "Ribeye Steak", "Chicken Parmesan", "Lobster Tail",
|
190
|
-
"Lamb Chops", "Fish Tacos", "Beef Stroganoff", "Chicken Alfredo",
|
191
|
-
"Pork Tenderloin", "Seafood Paella", "Duck Confit", "Vegetarian Lasagna",
|
192
|
-
"BBQ Ribs", "Fish & Chips", "Stuffed Peppers", "Beef Wellington"
|
193
|
-
]
|
194
|
-
dishes.each_with_index do |dish, index|
|
195
|
-
price = 18 + (index * 2)
|
196
|
-
items["dish_#{index}"] = "#{dish} - $#{price}"
|
197
|
-
end
|
198
|
-
items
|
199
|
-
when "desserts"
|
200
|
-
{
|
201
|
-
"tiramisu" => "Tiramisu - $8",
|
202
|
-
"cheesecake" => "New York Cheesecake - $7",
|
203
|
-
"chocolate_cake" => "Chocolate Lava Cake - $9",
|
204
|
-
"ice_cream" => "Artisan Ice Cream - $6"
|
205
|
-
}
|
206
|
-
when "beverages"
|
207
|
-
{
|
208
|
-
"wine_red" => "House Red Wine - $8",
|
209
|
-
"wine_white" => "House White Wine - $8",
|
210
|
-
"beer" => "Craft Beer - $6",
|
211
|
-
"cocktail" => "Signature Cocktail - $12",
|
212
|
-
"coffee" => "Espresso Coffee - $4",
|
213
|
-
"tea" => "Premium Tea - $3",
|
214
|
-
"juice" => "Fresh Juice - $5",
|
215
|
-
"soda" => "Soft Drinks - $3"
|
216
|
-
}
|
217
|
-
when "specials"
|
218
|
-
{
|
219
|
-
"special_1" => "Chef's Special Pasta - $22",
|
220
|
-
"special_2" => "Today's Catch - Market Price",
|
221
|
-
"special_3" => "Tasting Menu (5 courses) - $65"
|
222
|
-
}
|
223
|
-
else
|
224
|
-
{}
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
def show_item_details(category, item_key)
|
229
|
-
item_name = get_menu_items(category)[item_key]
|
230
|
-
|
231
|
-
app.say "You selected: #{item_name}",
|
232
|
-
media: {
|
233
|
-
type: :image,
|
234
|
-
url: "https://flowchat-demo.com/items/#{item_key}.jpg"
|
235
|
-
}
|
236
|
-
|
237
|
-
# Show detailed description with document menu
|
238
|
-
show_detailed_info = app.screen("#{item_key}_details".to_sym) do |prompt|
|
239
|
-
prompt.yes? "Would you like to see detailed nutritional information?"
|
240
|
-
end
|
241
|
-
|
242
|
-
if show_detailed_info
|
243
|
-
app.say "Here's the detailed information:",
|
244
|
-
media: {
|
245
|
-
type: :document,
|
246
|
-
url: "https://flowchat-demo.com/nutrition/#{item_key}.pdf",
|
247
|
-
filename: "#{item_key}_nutrition.pdf"
|
248
|
-
}
|
249
|
-
end
|
250
|
-
|
251
|
-
# Quantity selection
|
252
|
-
quantity = app.screen("#{item_key}_quantity".to_sym) do |prompt|
|
253
|
-
prompt.ask "How many would you like?",
|
254
|
-
convert: ->(input) { input.to_i },
|
255
|
-
validate: ->(qty) {
|
256
|
-
return "Quantity must be at least 1" if qty < 1
|
257
|
-
return "Maximum 10 items per order" if qty > 10
|
258
|
-
nil
|
259
|
-
}
|
260
|
-
end
|
261
|
-
|
262
|
-
# Special instructions
|
263
|
-
special_instructions = app.screen("#{item_key}_instructions".to_sym) do |prompt|
|
264
|
-
prompt.ask "Any special instructions? (or type 'none')",
|
265
|
-
transform: ->(input) { input.strip.downcase == 'none' ? nil : input.strip }
|
266
|
-
end
|
267
|
-
|
268
|
-
# Add to cart
|
269
|
-
add_to_cart(item_key, item_name, quantity, special_instructions)
|
270
|
-
|
271
|
-
# Continue shopping?
|
272
|
-
continue_shopping = app.screen(:continue_shopping) do |prompt|
|
273
|
-
prompt.yes? "Item added to cart! Continue shopping?"
|
274
|
-
end
|
275
|
-
|
276
|
-
if continue_shopping
|
277
|
-
browse_menu
|
278
|
-
else
|
279
|
-
view_cart
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
def add_to_cart(item_key, item_name, quantity, instructions)
|
284
|
-
cart = app.session.get(:cart) || []
|
285
|
-
|
286
|
-
cart_item = {
|
287
|
-
key: item_key,
|
288
|
-
name: item_name,
|
289
|
-
quantity: quantity,
|
290
|
-
instructions: instructions,
|
291
|
-
added_at: Time.current.iso8601
|
292
|
-
}
|
293
|
-
|
294
|
-
cart << cart_item
|
295
|
-
app.session.set(:cart, cart)
|
296
|
-
end
|
297
|
-
|
298
|
-
def view_cart
|
299
|
-
cart = app.session.get(:cart) || []
|
300
|
-
|
301
|
-
if cart.empty?
|
302
|
-
app.say "Your cart is empty! 🛒"
|
303
|
-
|
304
|
-
browse_now = app.screen(:browse_from_empty_cart) do |prompt|
|
305
|
-
prompt.yes? "Would you like to browse our menu?"
|
306
|
-
end
|
307
|
-
|
308
|
-
if browse_now
|
309
|
-
browse_menu
|
310
|
-
else
|
311
|
-
main_menu
|
312
|
-
end
|
313
|
-
return
|
314
|
-
end
|
315
|
-
|
316
|
-
# Show cart contents
|
317
|
-
cart_summary = build_cart_summary(cart)
|
318
|
-
app.say "Your Cart:\n\n#{cart_summary}"
|
319
|
-
|
320
|
-
# Cart actions
|
321
|
-
action = app.screen(:cart_action) do |prompt|
|
322
|
-
prompt.select "What would you like to do?", [
|
323
|
-
"Proceed to Checkout",
|
324
|
-
"Continue Shopping",
|
325
|
-
"Clear Cart",
|
326
|
-
"Remove Item"
|
327
|
-
]
|
328
|
-
end
|
329
|
-
|
330
|
-
case action
|
331
|
-
when "Proceed to Checkout"
|
332
|
-
checkout_process
|
333
|
-
when "Continue Shopping"
|
334
|
-
browse_menu
|
335
|
-
when "Clear Cart"
|
336
|
-
clear_cart
|
337
|
-
when "Remove Item"
|
338
|
-
remove_item_from_cart
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
def build_cart_summary(cart)
|
343
|
-
total = 0
|
344
|
-
summary = cart.map.with_index do |item, index|
|
345
|
-
# Extract price from item name (simplified)
|
346
|
-
price_match = item[:name].match(/\$(\d+)/)
|
347
|
-
price = price_match ? price_match[1].to_f : 0
|
348
|
-
item_total = price * item[:quantity]
|
349
|
-
total += item_total
|
350
|
-
|
351
|
-
instructions_text = item[:instructions] ? "\n Special: #{item[:instructions]}" : ""
|
352
|
-
"#{index + 1}. #{item[:name]} x#{item[:quantity]} = $#{item_total}#{instructions_text}"
|
353
|
-
end.join("\n")
|
354
|
-
|
355
|
-
summary + "\n\nTotal: $#{total.round(2)}"
|
356
|
-
end
|
357
|
-
|
358
|
-
def checkout_process
|
359
|
-
cart = app.session.get(:cart)
|
360
|
-
customer_data = app.session.get(:customer_data)
|
361
|
-
|
362
|
-
# Delivery or pickup
|
363
|
-
order_type = app.screen(:order_type) do |prompt|
|
364
|
-
prompt.select "How would you like your order?", {
|
365
|
-
"delivery" => "🚚 Delivery",
|
366
|
-
"pickup" => "🏃 Pickup"
|
367
|
-
}
|
368
|
-
end
|
369
|
-
|
370
|
-
if order_type == "delivery"
|
371
|
-
handle_delivery_address
|
372
|
-
else
|
373
|
-
handle_pickup_time
|
374
|
-
end
|
375
|
-
|
376
|
-
# Payment method
|
377
|
-
payment_method = app.screen(:payment_method) do |prompt|
|
378
|
-
prompt.select "Payment method:", {
|
379
|
-
"card" => "💳 Credit/Debit Card",
|
380
|
-
"cash" => "💵 Cash",
|
381
|
-
"mobile" => "📱 Mobile Payment"
|
382
|
-
}
|
383
|
-
end
|
384
|
-
|
385
|
-
if payment_method == "card"
|
386
|
-
handle_card_payment
|
387
|
-
end
|
388
|
-
|
389
|
-
# Order confirmation
|
390
|
-
order_confirmation
|
391
|
-
end
|
392
|
-
|
393
|
-
def handle_delivery_address
|
394
|
-
address = app.screen(:delivery_address) do |prompt|
|
395
|
-
prompt.ask "Enter your delivery address:",
|
396
|
-
validate: ->(addr) {
|
397
|
-
return "Address must be at least 10 characters" if addr.length < 10
|
398
|
-
nil
|
399
|
-
}
|
400
|
-
end
|
401
|
-
|
402
|
-
# Delivery time
|
403
|
-
delivery_time = app.screen(:delivery_time) do |prompt|
|
404
|
-
prompt.select "Preferred delivery time:", [
|
405
|
-
"ASAP (45-60 mins)",
|
406
|
-
"1-2 hours",
|
407
|
-
"2-3 hours",
|
408
|
-
"This evening",
|
409
|
-
"Schedule for later"
|
410
|
-
]
|
411
|
-
end
|
412
|
-
|
413
|
-
app.session.set(:delivery_info, {
|
414
|
-
type: "delivery",
|
415
|
-
address: address,
|
416
|
-
time: delivery_time
|
417
|
-
})
|
418
|
-
end
|
419
|
-
|
420
|
-
def handle_pickup_time
|
421
|
-
pickup_time = app.screen(:pickup_time) do |prompt|
|
422
|
-
prompt.select "When would you like to pick up?", [
|
423
|
-
"20-30 minutes",
|
424
|
-
"30-45 minutes",
|
425
|
-
"1 hour",
|
426
|
-
"Schedule for later"
|
427
|
-
]
|
428
|
-
end
|
429
|
-
|
430
|
-
app.session.set(:delivery_info, {
|
431
|
-
type: "pickup",
|
432
|
-
time: pickup_time
|
433
|
-
})
|
434
|
-
end
|
435
|
-
|
436
|
-
def handle_card_payment
|
437
|
-
# Card number validation (simplified for demo)
|
438
|
-
card_number = app.screen(:card_number) do |prompt|
|
439
|
-
prompt.ask "Enter card number (16 digits):",
|
440
|
-
transform: ->(input) { input.gsub(/\s/, '') },
|
441
|
-
validate: ->(card) {
|
442
|
-
return "Card number must be 16 digits" unless card.match?(/\A\d{16}\z/)
|
443
|
-
return "Invalid card number" unless luhn_valid?(card)
|
444
|
-
nil
|
445
|
-
}
|
446
|
-
end
|
447
|
-
|
448
|
-
# Expiry date
|
449
|
-
expiry = app.screen(:card_expiry) do |prompt|
|
450
|
-
prompt.ask "Expiry date (MM/YY):",
|
451
|
-
validate: ->(exp) {
|
452
|
-
return "Format must be MM/YY" unless exp.match?(/\A\d{2}\/\d{2}\z/)
|
453
|
-
month, year = exp.split('/').map(&:to_i)
|
454
|
-
return "Invalid month" unless (1..12).include?(month)
|
455
|
-
return "Card expired" if (2000 + year) < Date.current.year
|
456
|
-
nil
|
457
|
-
}
|
458
|
-
end
|
459
|
-
|
460
|
-
# CVV
|
461
|
-
cvv = app.screen(:card_cvv) do |prompt|
|
462
|
-
prompt.ask "CVV (3 digits):",
|
463
|
-
validate: ->(cvv) {
|
464
|
-
return "CVV must be 3 digits" unless cvv.match?(/\A\d{3}\z/)
|
465
|
-
nil
|
466
|
-
}
|
467
|
-
end
|
468
|
-
|
469
|
-
app.session.set(:payment_info, {
|
470
|
-
method: "card",
|
471
|
-
card_last_four: card_number[-4..-1],
|
472
|
-
status: "processed"
|
473
|
-
})
|
474
|
-
end
|
475
|
-
|
476
|
-
def order_confirmation
|
477
|
-
order_id = generate_order_id
|
478
|
-
cart = app.session.get(:cart)
|
479
|
-
customer_data = app.session.get(:customer_data)
|
480
|
-
delivery_info = app.session.get(:delivery_info)
|
481
|
-
payment_info = app.session.get(:payment_info)
|
482
|
-
|
483
|
-
# Save order
|
484
|
-
order_data = {
|
485
|
-
id: order_id,
|
486
|
-
customer: customer_data,
|
487
|
-
items: cart,
|
488
|
-
delivery: delivery_info,
|
489
|
-
payment: payment_info,
|
490
|
-
status: "confirmed",
|
491
|
-
created_at: Time.current.iso8601
|
492
|
-
}
|
493
|
-
|
494
|
-
orders = app.session.get(:orders) || []
|
495
|
-
orders << order_data
|
496
|
-
app.session.set(:orders, orders)
|
497
|
-
|
498
|
-
# Clear cart
|
499
|
-
app.session.set(:cart, [])
|
500
|
-
|
501
|
-
# Confirmation message with receipt
|
502
|
-
app.say "🎉 Order Confirmed!\n\nOrder ##{order_id}\nThank you, #{customer_data[:name]}!",
|
503
|
-
media: {
|
504
|
-
type: :document,
|
505
|
-
url: "https://flowchat-demo.com/receipts/#{order_id}.pdf",
|
506
|
-
filename: "receipt_#{order_id}.pdf"
|
507
|
-
}
|
508
|
-
|
509
|
-
# Send tracking info via audio message (WhatsApp feature)
|
510
|
-
app.say "Here's your order tracking information:",
|
511
|
-
media: {
|
512
|
-
type: :audio,
|
513
|
-
url: "https://flowchat-demo.com/tracking/#{order_id}.mp3"
|
514
|
-
}
|
515
|
-
|
516
|
-
# Ask for feedback
|
517
|
-
feedback_now = app.screen(:feedback_prompt) do |prompt|
|
518
|
-
prompt.yes? "Would you like to provide feedback about your ordering experience?"
|
519
|
-
end
|
520
|
-
|
521
|
-
if feedback_now
|
522
|
-
collect_feedback
|
523
|
-
else
|
524
|
-
post_order_options
|
525
|
-
end
|
526
|
-
end
|
527
|
-
|
528
|
-
def collect_feedback
|
529
|
-
rating = app.screen(:feedback_rating) do |prompt|
|
530
|
-
prompt.select "How would you rate your experience?", {
|
531
|
-
"5" => "⭐⭐⭐⭐⭐ Excellent",
|
532
|
-
"4" => "⭐⭐⭐⭐ Good",
|
533
|
-
"3" => "⭐⭐⭐ Average",
|
534
|
-
"2" => "⭐⭐ Poor",
|
535
|
-
"1" => "⭐ Very Poor"
|
536
|
-
}
|
537
|
-
end
|
538
|
-
|
539
|
-
if rating.to_i >= 4
|
540
|
-
app.say "Thank you for the great rating! 😊"
|
541
|
-
else
|
542
|
-
# Collect detailed feedback for lower ratings
|
543
|
-
feedback_details = app.screen(:feedback_details) do |prompt|
|
544
|
-
prompt.ask "We're sorry to hear that. Could you tell us what went wrong?",
|
545
|
-
validate: ->(feedback) {
|
546
|
-
return "Feedback must be at least 10 characters" if feedback.length < 10
|
547
|
-
nil
|
548
|
-
}
|
549
|
-
end
|
550
|
-
|
551
|
-
app.say "Thank you for your feedback. We'll use it to improve! 🙏"
|
552
|
-
end
|
553
|
-
|
554
|
-
post_order_options
|
555
|
-
end
|
556
|
-
|
557
|
-
def post_order_options
|
558
|
-
action = app.screen(:post_order_action) do |prompt|
|
559
|
-
prompt.select "What would you like to do now?", [
|
560
|
-
"Track Order",
|
561
|
-
"Order Again",
|
562
|
-
"Browse Menu",
|
563
|
-
"Contact Support",
|
564
|
-
"Exit"
|
565
|
-
]
|
566
|
-
end
|
567
|
-
|
568
|
-
case action
|
569
|
-
when "Track Order"
|
570
|
-
track_order
|
571
|
-
when "Order Again"
|
572
|
-
browse_menu
|
573
|
-
when "Browse Menu"
|
574
|
-
browse_menu
|
575
|
-
when "Contact Support"
|
576
|
-
contact_support
|
577
|
-
when "Exit"
|
578
|
-
app.say "Thank you for choosing FlowChat Restaurant! 👋"
|
579
|
-
end
|
580
|
-
end
|
581
|
-
|
582
|
-
def order_history
|
583
|
-
orders = app.session.get(:orders) || []
|
584
|
-
|
585
|
-
if orders.empty?
|
586
|
-
app.say "No previous orders found."
|
587
|
-
main_menu
|
588
|
-
return
|
589
|
-
end
|
590
|
-
|
591
|
-
order_list = orders.map.with_index do |order, index|
|
592
|
-
"#{index + 1}. Order ##{order[:id]} - #{order[:created_at]}"
|
593
|
-
end
|
594
|
-
|
595
|
-
selected_index = app.screen(:order_history_selection) do |prompt|
|
596
|
-
prompt.select "Select an order to view:", order_list.map.with_index { |order, i| [i, order] }.to_h
|
597
|
-
end
|
598
|
-
|
599
|
-
show_order_details(orders[selected_index])
|
600
|
-
end
|
601
|
-
|
602
|
-
def show_order_details(order)
|
603
|
-
details = "Order ##{order[:id]}\n"
|
604
|
-
details += "Status: #{order[:status].titleize}\n"
|
605
|
-
details += "Date: #{order[:created_at]}\n\n"
|
606
|
-
details += "Items:\n"
|
607
|
-
|
608
|
-
order[:items].each do |item|
|
609
|
-
details += "- #{item[:name]} x#{item[:quantity]}\n"
|
610
|
-
end
|
611
|
-
|
612
|
-
app.say details
|
613
|
-
|
614
|
-
action = app.screen(:order_detail_action) do |prompt|
|
615
|
-
prompt.select "What would you like to do?", [
|
616
|
-
"Track Order",
|
617
|
-
"Reorder Items",
|
618
|
-
"Contact Support",
|
619
|
-
"Back to Menu"
|
620
|
-
]
|
621
|
-
end
|
622
|
-
|
623
|
-
case action
|
624
|
-
when "Track Order"
|
625
|
-
track_order(order[:id])
|
626
|
-
when "Reorder Items"
|
627
|
-
reorder_items(order[:items])
|
628
|
-
when "Contact Support"
|
629
|
-
contact_support
|
630
|
-
when "Back to Menu"
|
631
|
-
main_menu
|
632
|
-
end
|
633
|
-
end
|
634
|
-
|
635
|
-
def track_order(order_id = nil)
|
636
|
-
order_id ||= app.session.get(:orders)&.last&.dig(:id)
|
637
|
-
|
638
|
-
if order_id.nil?
|
639
|
-
app.say "No orders to track."
|
640
|
-
main_menu
|
641
|
-
return
|
642
|
-
end
|
643
|
-
|
644
|
-
# Simulate tracking with video
|
645
|
-
app.say "Here's your order tracking:",
|
646
|
-
media: {
|
647
|
-
type: :video,
|
648
|
-
url: "https://flowchat-demo.com/tracking/#{order_id}.mp4"
|
649
|
-
}
|
650
|
-
|
651
|
-
app.say "📍 Order ##{order_id}\n🕐 Estimated time: 25 minutes\n📍 Status: Being prepared"
|
652
|
-
|
653
|
-
main_menu
|
654
|
-
end
|
655
|
-
|
656
|
-
def account_settings
|
657
|
-
customer_data = app.session.get(:customer_data)
|
658
|
-
|
659
|
-
setting = app.screen(:account_setting) do |prompt|
|
660
|
-
prompt.select "Account Settings:", [
|
661
|
-
"Update Name",
|
662
|
-
"Update Phone",
|
663
|
-
"Change Dietary Preferences",
|
664
|
-
"View Account Info",
|
665
|
-
"Delete Account"
|
666
|
-
]
|
667
|
-
end
|
668
|
-
|
669
|
-
case setting
|
670
|
-
when "Update Name"
|
671
|
-
update_customer_name
|
672
|
-
when "Update Phone"
|
673
|
-
update_customer_phone
|
674
|
-
when "Change Dietary Preferences"
|
675
|
-
update_dietary_preferences
|
676
|
-
when "View Account Info"
|
677
|
-
show_account_info
|
678
|
-
when "Delete Account"
|
679
|
-
delete_account
|
680
|
-
end
|
681
|
-
end
|
682
|
-
|
683
|
-
def update_customer_name
|
684
|
-
new_name = app.screen(:update_name) do |prompt|
|
685
|
-
prompt.ask "Enter your new name:",
|
686
|
-
transform: ->(input) { input.strip.titleize },
|
687
|
-
validate: ->(input) {
|
688
|
-
return "Name must be at least 2 characters" if input.length < 2
|
689
|
-
nil
|
690
|
-
}
|
691
|
-
end
|
692
|
-
|
693
|
-
customer_data = app.session.get(:customer_data)
|
694
|
-
customer_data[:name] = new_name
|
695
|
-
app.session.set(:customer_data, customer_data)
|
696
|
-
|
697
|
-
app.say "Name updated successfully! ✅"
|
698
|
-
account_settings
|
699
|
-
end
|
700
|
-
|
701
|
-
def contact_support
|
702
|
-
issue_type = app.screen(:support_issue_type) do |prompt|
|
703
|
-
prompt.select "What can we help you with?", {
|
704
|
-
"order_issue" => "Order Issue",
|
705
|
-
"payment_problem" => "Payment Problem",
|
706
|
-
"delivery_issue" => "Delivery Issue",
|
707
|
-
"food_quality" => "Food Quality Concern",
|
708
|
-
"technical_support" => "Technical Support",
|
709
|
-
"general_inquiry" => "General Inquiry"
|
710
|
-
}
|
711
|
-
end
|
712
|
-
|
713
|
-
# Collect issue description
|
714
|
-
description = app.screen(:support_description) do |prompt|
|
715
|
-
prompt.ask "Please describe your issue:",
|
716
|
-
validate: ->(desc) {
|
717
|
-
return "Description must be at least 20 characters" if desc.length < 20
|
718
|
-
nil
|
719
|
-
}
|
720
|
-
end
|
721
|
-
|
722
|
-
# Contact preference
|
723
|
-
contact_method = app.screen(:support_contact_method) do |prompt|
|
724
|
-
prompt.select "How would you like us to contact you?", {
|
725
|
-
"whatsapp" => "📱 WhatsApp",
|
726
|
-
"phone" => "📞 Phone Call",
|
727
|
-
"email" => "📧 Email"
|
728
|
-
}
|
729
|
-
end
|
730
|
-
|
731
|
-
# Generate support ticket
|
732
|
-
ticket_id = "SUP#{Time.current.to_i}"
|
733
|
-
|
734
|
-
# Confirmation with support document
|
735
|
-
app.say "Support ticket created: ##{ticket_id}\n\nWe'll contact you within 24 hours via #{contact_method}.",
|
736
|
-
media: {
|
737
|
-
type: :document,
|
738
|
-
url: "https://flowchat-demo.com/support/#{ticket_id}.pdf",
|
739
|
-
filename: "support_ticket_#{ticket_id}.pdf"
|
740
|
-
}
|
741
|
-
|
742
|
-
main_menu
|
743
|
-
end
|
744
|
-
|
745
|
-
def clear_cart
|
746
|
-
confirm = app.screen(:confirm_clear_cart) do |prompt|
|
747
|
-
prompt.yes? "Are you sure you want to clear your cart?"
|
748
|
-
end
|
749
|
-
|
750
|
-
if confirm
|
751
|
-
app.session.set(:cart, [])
|
752
|
-
app.say "Cart cleared! 🗑️"
|
753
|
-
end
|
754
|
-
|
755
|
-
main_menu
|
756
|
-
end
|
757
|
-
|
758
|
-
def remove_item_from_cart
|
759
|
-
cart = app.session.get(:cart) || []
|
760
|
-
|
761
|
-
if cart.empty?
|
762
|
-
app.say "Cart is already empty!"
|
763
|
-
main_menu
|
764
|
-
return
|
765
|
-
end
|
766
|
-
|
767
|
-
# Show items to remove
|
768
|
-
item_options = cart.map.with_index do |item, index|
|
769
|
-
[index, "#{index + 1}. #{item[:name]} x#{item[:quantity]}"]
|
770
|
-
end.to_h
|
771
|
-
|
772
|
-
selected_index = app.screen(:remove_item_selection) do |prompt|
|
773
|
-
prompt.select "Which item would you like to remove?", item_options
|
774
|
-
end
|
775
|
-
|
776
|
-
removed_item = cart.delete_at(selected_index)
|
777
|
-
app.session.set(:cart, cart)
|
778
|
-
|
779
|
-
app.say "Removed: #{removed_item[:name]} ❌"
|
780
|
-
view_cart
|
781
|
-
end
|
782
|
-
|
783
|
-
# Helper methods
|
784
|
-
def generate_order_id
|
785
|
-
"ORD#{Time.current.to_i}#{rand(100..999)}"
|
786
|
-
end
|
787
|
-
|
788
|
-
def luhn_valid?(card_number)
|
789
|
-
# Simplified Luhn algorithm for demo
|
790
|
-
digits = card_number.chars.map(&:to_i).reverse
|
791
|
-
sum = digits.each_with_index.sum do |digit, index|
|
792
|
-
if index.odd?
|
793
|
-
doubled = digit * 2
|
794
|
-
doubled > 9 ? doubled - 9 : doubled
|
795
|
-
else
|
796
|
-
digit
|
797
|
-
end
|
798
|
-
end
|
799
|
-
sum % 10 == 0
|
800
|
-
end
|
801
|
-
|
802
|
-
def reorder_items(items)
|
803
|
-
# Add items back to cart
|
804
|
-
cart = app.session.get(:cart) || []
|
805
|
-
items.each { |item| cart << item }
|
806
|
-
app.session.set(:cart, cart)
|
807
|
-
|
808
|
-
app.say "Items added to your cart! 🛒"
|
809
|
-
view_cart
|
810
|
-
end
|
811
|
-
|
812
|
-
def show_account_info
|
813
|
-
customer_data = app.session.get(:customer_data)
|
814
|
-
orders = app.session.get(:orders) || []
|
815
|
-
|
816
|
-
info = "Account Information:\n\n"
|
817
|
-
info += "Name: #{customer_data[:name]}\n"
|
818
|
-
info += "Phone: #{customer_data[:phone]}\n"
|
819
|
-
info += "Age: #{customer_data[:age]}\n"
|
820
|
-
info += "Dietary Preference: #{customer_data[:dietary_preference].titleize}\n"
|
821
|
-
info += "Total Orders: #{orders.length}\n"
|
822
|
-
info += "Member Since: #{customer_data[:registered_at]}"
|
823
|
-
|
824
|
-
app.say info
|
825
|
-
account_settings
|
826
|
-
end
|
827
|
-
|
828
|
-
def update_dietary_preferences
|
829
|
-
new_preference = app.screen(:update_dietary_preference) do |prompt|
|
830
|
-
prompt.select "Update dietary preferences:", {
|
831
|
-
"none" => "No restrictions",
|
832
|
-
"vegetarian" => "Vegetarian",
|
833
|
-
"vegan" => "Vegan",
|
834
|
-
"gluten_free" => "Gluten-free",
|
835
|
-
"keto" => "Keto",
|
836
|
-
"halal" => "Halal"
|
837
|
-
}
|
838
|
-
end
|
839
|
-
|
840
|
-
customer_data = app.session.get(:customer_data)
|
841
|
-
customer_data[:dietary_preference] = new_preference
|
842
|
-
app.session.set(:customer_data, customer_data)
|
843
|
-
|
844
|
-
app.say "Dietary preferences updated! ✅"
|
845
|
-
account_settings
|
846
|
-
end
|
847
|
-
|
848
|
-
def update_customer_phone
|
849
|
-
new_phone = app.screen(:update_phone) do |prompt|
|
850
|
-
prompt.ask "Enter your new phone number:",
|
851
|
-
transform: ->(input) { input.strip.gsub(/[\s\-\(\)]/, '') },
|
852
|
-
validate: ->(input) {
|
853
|
-
return "Phone number must start with +" unless input.start_with?('+')
|
854
|
-
return "Phone number must be 8-15 digits after +" unless input[1..-1].match?(/\A\d{8,15}\z/)
|
855
|
-
nil
|
856
|
-
}
|
857
|
-
end
|
858
|
-
|
859
|
-
customer_data = app.session.get(:customer_data)
|
860
|
-
customer_data[:phone] = new_phone
|
861
|
-
app.session.set(:customer_data, customer_data)
|
862
|
-
|
863
|
-
app.say "Phone number updated successfully! ✅"
|
864
|
-
account_settings
|
865
|
-
end
|
866
|
-
|
867
|
-
def delete_account
|
868
|
-
confirm = app.screen(:confirm_delete_account) do |prompt|
|
869
|
-
prompt.yes? "⚠️ Are you sure you want to delete your account? This cannot be undone."
|
870
|
-
end
|
871
|
-
|
872
|
-
if confirm
|
873
|
-
double_confirm = app.screen(:double_confirm_delete) do |prompt|
|
874
|
-
prompt.yes? "This will permanently delete all your data. Are you absolutely sure?"
|
875
|
-
end
|
876
|
-
|
877
|
-
if double_confirm
|
878
|
-
# Clear all session data
|
879
|
-
app.session.clear
|
880
|
-
app.say "Your account has been deleted. Thank you for using FlowChat Restaurant! 👋"
|
881
|
-
else
|
882
|
-
app.say "Account deletion cancelled."
|
883
|
-
account_settings
|
884
|
-
end
|
885
|
-
else
|
886
|
-
account_settings
|
887
|
-
end
|
888
|
-
end
|
889
|
-
end
|