Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -24,6 +24,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
| 24 |
from sqlalchemy.orm import sessionmaker, declarative_base
|
| 25 |
from sqlalchemy import Column, Integer, String, DateTime, Text, Float
|
| 26 |
|
|
|
|
| 27 |
SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "default_fallback_value")
|
| 28 |
PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "default_fallback_value")
|
| 29 |
DATABASE_URL = os.getenv("DATABASE_URL", "default_fallback_value")
|
|
@@ -35,13 +36,13 @@ WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_value"
|
|
| 35 |
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN", "default_value")
|
| 36 |
MANAGEMENT_WHATSAPP_NUMBER = os.getenv("MANAGEMENT_WHATSAPP_NUMBER", "default_value")
|
| 37 |
|
|
|
|
| 38 |
TOWN_SHIPPING_COSTS = {
|
| 39 |
-
"
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"default": 1000
|
| 45 |
}
|
| 46 |
|
| 47 |
Base = declarative_base()
|
|
@@ -99,7 +100,7 @@ class MenuItem(Base):
|
|
| 99 |
id = Column(Integer, primary_key=True, index=True)
|
| 100 |
name = Column(String, unique=True, index=True)
|
| 101 |
description = Column(Text)
|
| 102 |
-
price = Column(Integer) # Price
|
| 103 |
nutrition = Column(Text)
|
| 104 |
|
| 105 |
class TownShippingCost(Base):
|
|
@@ -108,7 +109,6 @@ class TownShippingCost(Base):
|
|
| 108 |
town = Column(String, unique=True, index=True)
|
| 109 |
cost = Column(Integer)
|
| 110 |
|
| 111 |
-
|
| 112 |
engine = create_async_engine(DATABASE_URL, echo=True)
|
| 113 |
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
| 114 |
|
|
@@ -120,65 +120,28 @@ user_state = {}
|
|
| 120 |
conversation_context = {}
|
| 121 |
proactive_timer = {}
|
| 122 |
|
|
|
|
| 123 |
menu_items = [
|
| 124 |
-
{"name": "
|
| 125 |
-
{"name": "
|
| 126 |
-
{"name": "
|
| 127 |
-
{"name": "
|
| 128 |
-
{"name": "
|
| 129 |
-
{"name": "
|
| 130 |
-
{"name": "
|
| 131 |
-
{"name": "
|
| 132 |
-
{"name": "
|
| 133 |
-
{"name": "
|
| 134 |
-
{"name": "Chicken Wings", "description": "Crispy fried chicken wings", "price": 3960, "nutrition": "Calories: 450 kcal, Carbs: 20g, Protein: 30g, Fat: 20g"},
|
| 135 |
-
{"name": "Fried Chicken", "description": "Crispy deep-fried chicken", "price": 4140, "nutrition": "Calories: 500 kcal, Carbs: 15g, Protein: 35g, Fat: 25g"},
|
| 136 |
-
{"name": "Turkey", "description": "Crispy deep-fried turkey", "price": 4000, "nutrition": "Calories: 480 kcal, Carbs: 10g, Protein: 40g, Fat: 20g"},
|
| 137 |
-
{"name": "Gizzard", "description": "Fried or grilled gizzard", "price": 3000, "nutrition": "Calories: 200 kcal, Carbs: 5g, Protein: 20g, Fat: 8g"},
|
| 138 |
-
{"name": "Fish", "description": "Fried or grilled fish", "price": 2500, "nutrition": "Calories: 250 kcal, Carbs: 5g, Protein: 25g, Fat: 10g"},
|
| 139 |
-
{"name": "Beef", "description": "Spicy fried or grilled beef", "price": 1200, "nutrition": "Calories: 300 kcal, Carbs: 2g, Protein: 28g, Fat: 15g"},
|
| 140 |
-
{"name": "Pepper Chicken", "description": "Spicy peppered chicken", "price": 1500, "nutrition": "Calories: 400 kcal, Carbs: 10g, Protein: 35g, Fat: 18g"},
|
| 141 |
-
{"name": "Pepper Chicken Big", "description": "Larger portion of spicy peppered chicken", "price": 2500, "nutrition": "Calories: 600 kcal, Carbs: 15g, Protein: 50g, Fat: 25g"},
|
| 142 |
-
{"name": "Salad", "description": "Fresh vegetable salad", "price": 800, "nutrition": "Calories: 100 kcal, Carbs: 15g, Protein: 3g, Fat: 5g"},
|
| 143 |
-
{"name": "Egg and Yam", "description": "Boiled yam served with eggs", "price": 1000, "nutrition": "Calories: 300 kcal, Carbs: 50g, Protein: 10g, Fat: 5g"},
|
| 144 |
-
{"name": "Moi Moi", "description": "Steamed bean pudding", "price": 1000, "nutrition": "Calories: 250 kcal, Carbs: 30g, Protein: 12g, Fat: 8g"},
|
| 145 |
-
{"name": "Plantain", "description": "Fried or boiled ripe plantain", "price": 1000, "nutrition": "Calories: 200 kcal, Carbs: 50g, Protein: 2g, Fat: 1g"},
|
| 146 |
-
{"name": "Soup with Goat Meat and Semo", "description": "Rich soup with goat meat served with semo", "price": 4600, "nutrition": "Calories: 500 kcal, Carbs: 60g, Protein: 30g, Fat: 15g"},
|
| 147 |
-
{"name": "Soup with Chicken and Semo", "description": "Soup served with chicken and semo", "price": 500, "nutrition": "Calories: 450 kcal, Carbs: 55g, Protein: 28g, Fat: 12g"},
|
| 148 |
-
{"name": "Extra Semo", "description": "Additional portion of semo", "price": 500, "nutrition": "Calories: 200 kcal, Carbs: 45g, Protein: 5g, Fat: 1g"},
|
| 149 |
-
{"name": "Banga Soup", "description": "Traditional dish with catfish", "price": 5000, "nutrition": "Calories: 600 kcal, Carbs: 50g, Protein: 35g, Fat: 15g"}
|
| 150 |
]
|
| 151 |
|
| 152 |
-
|
| 153 |
-
{"name": "
|
| 154 |
-
{"name": "
|
| 155 |
-
{"name": "
|
| 156 |
-
{"name": "
|
| 157 |
-
{"name": "Bottled Water", "description": "Pure, refreshing drinking water", "price": 500, "nutrition": "Calories: 0 kcal, Carbs: 0g, Protein: 0g, Fat: 0g"},
|
| 158 |
-
{"name": "Energy Drink", "description": "Boost of energy with caffeine and vitamins", "price": 1500, "nutrition": "Calories: 200 kcal, Carbs: 50g, Protein: 1g, Fat: 0g"}
|
| 159 |
]
|
| 160 |
|
| 161 |
-
menu_items.extend(
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
class ConversationState:
|
| 165 |
-
def __init__(self):
|
| 166 |
-
self.flow = None
|
| 167 |
-
self.step = 0
|
| 168 |
-
self.data = {}
|
| 169 |
-
self.last_active = datetime.utcnow()
|
| 170 |
-
|
| 171 |
-
def update_last_active(self):
|
| 172 |
-
self.last_active = datetime.utcnow()
|
| 173 |
-
|
| 174 |
-
def is_expired(self):
|
| 175 |
-
return datetime.utcnow() - self.last_active > SESSION_TIMEOUT
|
| 176 |
-
|
| 177 |
-
def reset(self):
|
| 178 |
-
self.flow = None
|
| 179 |
-
self.step = 0
|
| 180 |
-
self.data = {}
|
| 181 |
-
self.last_active = datetime.utcnow()
|
| 182 |
|
| 183 |
SESSION_TIMEOUT = timedelta(minutes=5)
|
| 184 |
|
|
@@ -212,7 +175,6 @@ async def populate_shipping_costs():
|
|
| 212 |
session.add(new_cost)
|
| 213 |
await session.commit()
|
| 214 |
|
| 215 |
-
|
| 216 |
async def log_chat_to_db(user_id: str, direction: str, message: str):
|
| 217 |
async with async_session() as session:
|
| 218 |
entry = ChatHistory(user_id=user_id, direction=direction, message=message)
|
|
@@ -255,7 +217,7 @@ def create_paystack_payment_link(email: str, amount: int, reference: str) -> dic
|
|
| 255 |
"email": email,
|
| 256 |
"amount": amount,
|
| 257 |
"reference": reference,
|
| 258 |
-
"callback_url": "https://
|
| 259 |
}
|
| 260 |
try:
|
| 261 |
response = requests.post(url, json=data, headers=headers, timeout=10)
|
|
@@ -346,8 +308,8 @@ def calculate_shipping_cost(address: str) -> int:
|
|
| 346 |
def calculate_eta(destination: str) -> str:
|
| 347 |
if not GOOGLE_MAPS_API_KEY:
|
| 348 |
return "ETA unavailable (Google Maps API key missing)."
|
| 349 |
-
|
| 350 |
-
origin = "
|
| 351 |
url = f"https://maps.googleapis.com/maps/api/directions/json?origin={origin}&destination={destination}&key={GOOGLE_MAPS_API_KEY}"
|
| 352 |
|
| 353 |
try:
|
|
@@ -398,7 +360,6 @@ async def get_menu_items():
|
|
| 398 |
for item in items
|
| 399 |
]
|
| 400 |
|
| 401 |
-
|
| 402 |
async def track_order(user_id: str, order_id: str) -> str:
|
| 403 |
async with async_session() as session:
|
| 404 |
order_result = await session.execute(
|
|
@@ -475,24 +436,18 @@ def match_dishes(user_input: str, threshold: int = 70) -> list:
|
|
| 475 |
Then, try an exact whole-word match using a regex with word boundaries.
|
| 476 |
If no exact match is found, fall back to fuzzy matching.
|
| 477 |
"""
|
| 478 |
-
# Normalize input: lowercase, strip, and collapse spaces
|
| 479 |
normalized_input = re.sub(r'\s+', ' ', user_input.lower().strip())
|
| 480 |
matched_dishes = []
|
| 481 |
|
| 482 |
-
# Try exact matching with word boundaries.
|
| 483 |
for item in menu_items:
|
| 484 |
dish_name = item["name"]
|
| 485 |
-
# Create a pattern with word boundaries; e.g., for "fried rice":
|
| 486 |
-
# pattern becomes: r'\bfried rice\b'
|
| 487 |
pattern = r'\b' + re.escape(dish_name.lower()) + r'\b'
|
| 488 |
if re.search(pattern, normalized_input):
|
| 489 |
matched_dishes.append(dish_name)
|
| 490 |
|
| 491 |
-
# If we found any exact matches, return them.
|
| 492 |
if matched_dishes:
|
| 493 |
return list(set(matched_dishes))
|
| 494 |
|
| 495 |
-
# If no exact match, fall back to fuzzy matching.
|
| 496 |
for item in menu_items:
|
| 497 |
dish_name = item["name"]
|
| 498 |
score = fuzz.token_sort_ratio(normalized_input, dish_name.lower())
|
|
@@ -501,61 +456,48 @@ def match_dishes(user_input: str, threshold: int = 70) -> list:
|
|
| 501 |
|
| 502 |
return list(set(matched_dishes))
|
| 503 |
|
| 504 |
-
#
|
| 505 |
-
# for item in menu_items:
|
| 506 |
-
# if item["name"].lower() == dish.lower():
|
| 507 |
-
# return item["price"]
|
| 508 |
-
# return 0 # or raise an error if dish not found
|
| 509 |
-
|
| 510 |
-
# Fetch dish price from the MenuItem table
|
| 511 |
async def get_dish_price(dish: str) -> int:
|
| 512 |
async with async_session() as session:
|
| 513 |
-
# Using ilike() for case-insensitive matching
|
| 514 |
result = await session.execute(select(MenuItem).where(MenuItem.name.ilike(dish)))
|
| 515 |
menu_item = result.scalars().first()
|
| 516 |
return menu_item.price if menu_item else 0
|
| 517 |
|
| 518 |
-
# Fetch shipping cost from the TownShippingCost table based on the address.
|
| 519 |
async def get_shipping_cost(address: str) -> int:
|
| 520 |
async with async_session() as session:
|
| 521 |
result = await session.execute(select(TownShippingCost))
|
| 522 |
costs = result.scalars().all()
|
| 523 |
address_lower = address.lower()
|
| 524 |
-
# Look for a matching town in the address.
|
| 525 |
for cost in costs:
|
| 526 |
if cost.town in address_lower:
|
| 527 |
return cost.cost
|
| 528 |
-
# Fallback: return the default cost.
|
| 529 |
default_cost = next((c for c in costs if c.town == "default"), None)
|
| 530 |
-
return default_cost.cost if default_cost else
|
| 531 |
-
|
| 532 |
|
| 533 |
def send_email_notification(order_details):
|
| 534 |
-
#
|
| 535 |
payload = {
|
| 536 |
-
"from": "
|
| 537 |
-
"to": "
|
| 538 |
"subject": f"New Order Received: {order_details['order_id']}",
|
| 539 |
"body": (
|
| 540 |
f"New Order Received:\n"
|
| 541 |
f"Order ID: {order_details['order_id']}\n"
|
| 542 |
f"Dish: {order_details['dish']}\n"
|
| 543 |
f"Quantity: {order_details['quantity']}\n"
|
| 544 |
-
f"Total Price:
|
| 545 |
f"Phone: {order_details.get('phone_number', 'Not Provided')}\n"
|
| 546 |
f"Delivery Address: {order_details.get('address', 'Not Provided')}\n"
|
| 547 |
f"Extras: {order_details.get('extras', 'None')}\n"
|
| 548 |
f"Status: Pending Payment"
|
| 549 |
),
|
| 550 |
-
|
| 551 |
-
"
|
| 552 |
-
"
|
| 553 |
-
"
|
| 554 |
-
"
|
| 555 |
-
"smtpPassword": "uddvxabxotlvfewk",
|
| 556 |
}
|
| 557 |
|
| 558 |
-
# Replace the URL with the URL of your SMTP API endpoint.
|
| 559 |
url = "https://smtp-server-ten.vercel.app/smtp"
|
| 560 |
try:
|
| 561 |
response = requests.post(url, json=payload, timeout=10)
|
|
@@ -565,29 +507,6 @@ def send_email_notification(order_details):
|
|
| 565 |
print(f"Error sending email: {e}")
|
| 566 |
return None
|
| 567 |
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
def match_dishes(user_input: str, threshold: int = 70) -> list:
|
| 571 |
-
matched_dishes = []
|
| 572 |
-
user_input_lower = user_input.lower()
|
| 573 |
-
for item in menu_items:
|
| 574 |
-
dish_name = item["name"]
|
| 575 |
-
# Direct substring check
|
| 576 |
-
if dish_name.lower() in user_input_lower:
|
| 577 |
-
matched_dishes.append(dish_name)
|
| 578 |
-
else:
|
| 579 |
-
score = fuzz.token_sort_ratio(user_input_lower, dish_name.lower())
|
| 580 |
-
if score >= threshold:
|
| 581 |
-
matched_dishes.append(dish_name)
|
| 582 |
-
return list(set(matched_dishes))
|
| 583 |
-
|
| 584 |
-
def get_dish_price(dish: str) -> int:
|
| 585 |
-
for item in menu_items:
|
| 586 |
-
if item["name"].lower() == dish.lower():
|
| 587 |
-
return item["price"]
|
| 588 |
-
return 0
|
| 589 |
-
|
| 590 |
-
|
| 591 |
async def process_order_flow(user_id: str, message: str) -> str:
|
| 592 |
"""
|
| 593 |
Processes order flow allowing an unlimited number of dishes.
|
|
@@ -598,16 +517,14 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 598 |
del user_state[user_id]
|
| 599 |
state = None
|
| 600 |
|
| 601 |
-
# 1)
|
| 602 |
if message.lower() in ["order", "menu"]:
|
| 603 |
state = ConversationState()
|
| 604 |
state.flow = "order"
|
| 605 |
state.step = 1
|
| 606 |
state.update_last_active()
|
| 607 |
user_state[user_id] = state
|
| 608 |
-
|
| 609 |
-
return "Sure! What dish would you like to order?"
|
| 610 |
-
return ""
|
| 611 |
|
| 612 |
if not state and "order" in message.lower():
|
| 613 |
state = ConversationState()
|
|
@@ -615,14 +532,13 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 615 |
state.step = 1
|
| 616 |
state.update_last_active()
|
| 617 |
user_state[user_id] = state
|
| 618 |
-
return "
|
| 619 |
|
| 620 |
-
# 2)
|
| 621 |
if not state or state.flow != "order":
|
| 622 |
matched = match_dishes(message)
|
| 623 |
if matched:
|
| 624 |
if len(matched) == 1:
|
| 625 |
-
# Single dish order
|
| 626 |
found_dish = matched[0]
|
| 627 |
state = ConversationState()
|
| 628 |
state.flow = "order"
|
|
@@ -648,13 +564,12 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 648 |
state.step = 5
|
| 649 |
return (f"Thanks! Your phone number is recorded as: {state.data['phone_number']}.\n"
|
| 650 |
f"Your delivery address is: {state.data['address']}.\n"
|
| 651 |
-
f"
|
| 652 |
elif state.data.get("phone_number") and not state.data.get("address"):
|
| 653 |
-
return "Thank you. Please provide your delivery address."
|
| 654 |
else:
|
| 655 |
-
return "Please provide your phone number and address."
|
| 656 |
else:
|
| 657 |
-
# Multiple dishes detected – store all in candidate_dishes.
|
| 658 |
state = ConversationState()
|
| 659 |
state.flow = "order"
|
| 660 |
state.update_last_active()
|
|
@@ -667,7 +582,6 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 667 |
else:
|
| 668 |
return "I couldn't identify the dish. Please type the dish name from our menu."
|
| 669 |
|
| 670 |
-
# --- Candidate Dishes Clarification Branch ---
|
| 671 |
if state and state.flow == "order" and "candidate_dishes" in state.data:
|
| 672 |
normalized = message.strip().lower()
|
| 673 |
if normalized in ["both", "all"]:
|
|
@@ -676,7 +590,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 676 |
state.step = 2
|
| 677 |
dishes_str = ", ".join(state.data["dishes"])
|
| 678 |
return (f"You have selected: {dishes_str}. How many servings of each would you like? "
|
| 679 |
-
"(For example, '2 for
|
| 680 |
else:
|
| 681 |
for dish in state.data["candidate_dishes"]:
|
| 682 |
if dish.lower() in normalized:
|
|
@@ -688,16 +602,13 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 688 |
return (f"Please specify which one you'd like to order from: {dish_options} "
|
| 689 |
"(or type 'both' if you'd like to order all).")
|
| 690 |
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
# 3) If state exists and we're at step 2: parse quantity details.
|
| 694 |
if state and state.flow == "order" and state.step == 2:
|
| 695 |
-
# For multi-dish orders, expect input like "2 for Jollof Rice, 3 for Chicken Wings, 1 for Fish"
|
| 696 |
if "dishes" in state.data and len(state.data["dishes"]) > 1:
|
| 697 |
pairs = re.findall(r'(\d+)\s*for\s*([a-zA-Z\s]+)', message, flags=re.IGNORECASE)
|
| 698 |
if not pairs:
|
| 699 |
return ("I'm sorry, I didn't understand the quantity details. "
|
| 700 |
-
"Please specify like '2 for
|
| 701 |
order_quantities = {}
|
| 702 |
for quantity, dish_text in pairs:
|
| 703 |
dish_text = dish_text.strip().lower()
|
|
@@ -709,11 +620,10 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 709 |
state.step = 3
|
| 710 |
summary = "\n".join([f"{q} serving(s) of {d}" for d, q in order_quantities.items()])
|
| 711 |
return (f"Got it. You have ordered:\n{summary}\n"
|
| 712 |
-
"Please provide your phone number and delivery address.")
|
| 713 |
else:
|
| 714 |
return ("I'm sorry, I couldn't match those dishes. "
|
| 715 |
-
"Please try something like '2 for
|
| 716 |
-
# For single-dish order at step 2: user types just a quantity.
|
| 717 |
numbers = re.findall(r'\d+', message)
|
| 718 |
if not numbers:
|
| 719 |
return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
|
|
@@ -724,10 +634,9 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 724 |
state.step = 3
|
| 725 |
dish = state.data.get("dish", "")
|
| 726 |
return (f"Got it. {quantity} serving(s) of {dish}.\n"
|
| 727 |
-
"Please provide your phone number and delivery address.")
|
| 728 |
|
| 729 |
-
# Step
|
| 730 |
-
# --- Step 4: Parse phone & address, compute shipping cost, and jump to confirmation ---
|
| 731 |
if state and state.flow == "order" and state.step == 3:
|
| 732 |
phone_pattern = r'(\+?\d{10,15})'
|
| 733 |
phone_match = re.search(phone_pattern, message)
|
|
@@ -744,67 +653,57 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 744 |
state.data["address"] = message.strip()
|
| 745 |
else:
|
| 746 |
return ("Please provide both your phone number and address. "
|
| 747 |
-
"For example: '
|
| 748 |
if not state.data.get("address"):
|
| 749 |
return "Thank you. Please provide your delivery address."
|
| 750 |
-
# Get shipping cost from the database
|
| 751 |
shipping_cost = await get_shipping_cost(state.data["address"])
|
| 752 |
state.data["shipping_cost"] = shipping_cost
|
| 753 |
-
|
| 754 |
-
# Automatically set extras to empty (since we're not using extras)
|
| 755 |
state.data["extras"] = ""
|
| 756 |
-
|
| 757 |
-
# Jump directly to order confirmation (Step 7)
|
| 758 |
state.step = 7
|
| 759 |
-
|
| 760 |
-
# For order summary, determine dish summary and quantity:
|
| 761 |
if "orders" in state.data:
|
| 762 |
dish_summary = ", ".join(state.data["orders"].keys())
|
| 763 |
quantity_total = sum(state.data["orders"].values())
|
| 764 |
total_price = 0
|
| 765 |
for dish, qty in state.data["orders"].items():
|
| 766 |
-
|
|
|
|
| 767 |
total_price += shipping_cost
|
| 768 |
else:
|
| 769 |
dish_summary = state.data.get("dish", "")
|
| 770 |
quantity_total = state.data.get("quantity", 1)
|
| 771 |
-
dish_price = get_dish_price(dish_summary)
|
| 772 |
total_price = (quantity_total * dish_price) + shipping_cost
|
| 773 |
-
|
| 774 |
return (f"Order Summary:\n"
|
| 775 |
f"Dish(es): {dish_summary}\n"
|
| 776 |
f"Quantity: {quantity_total}\n"
|
| 777 |
f"Phone: {state.data.get('phone_number', '')}\n"
|
| 778 |
f"Address: {state.data.get('address', '')}\n"
|
| 779 |
-
f"
|
| 780 |
-
f"Total Price:
|
| 781 |
"Confirm order? (yes/no)")
|
| 782 |
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
# 7) Step 7: Order Confirmation and Payment Link Generation
|
| 787 |
if state and state.flow == "order" and state.step == 7:
|
| 788 |
if message.lower() in ["yes", "y"]:
|
| 789 |
order_id = f"ORD-{int(time.time())}"
|
| 790 |
state.data["order_id"] = order_id
|
| 791 |
-
|
| 792 |
-
# Calculate total price:
|
| 793 |
if "orders" in state.data:
|
| 794 |
total_price = 0
|
| 795 |
for dish, qty in state.data["orders"].items():
|
| 796 |
-
|
|
|
|
| 797 |
total_price += state.data.get("shipping_cost", 0)
|
| 798 |
dish_summary = ", ".join(state.data["orders"].keys())
|
| 799 |
quantity_total = sum(state.data["orders"].values())
|
| 800 |
else:
|
| 801 |
dish_summary = state.data.get("dish", "")
|
| 802 |
quantity_total = state.data.get("quantity", 1)
|
| 803 |
-
dish_price = get_dish_price(dish_summary)
|
| 804 |
total_price = (quantity_total * dish_price) + state.data.get("shipping_cost", 0)
|
| 805 |
|
| 806 |
state.data["price"] = str(total_price)
|
| 807 |
-
|
| 808 |
async def save_order():
|
| 809 |
async with async_session() as session:
|
| 810 |
order = Order(
|
|
@@ -820,8 +719,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 820 |
await session.commit()
|
| 821 |
asyncio.create_task(save_order())
|
| 822 |
asyncio.create_task(log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment."))
|
| 823 |
-
|
| 824 |
-
# Prepare order details for email notification.
|
| 825 |
order_details = {
|
| 826 |
"order_id": order_id,
|
| 827 |
"dish": dish_summary,
|
|
@@ -831,48 +729,27 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 831 |
"address": state.data.get("address", "Not Provided"),
|
| 832 |
"extras": state.data.get("extras", "None")
|
| 833 |
}
|
| 834 |
-
admin_emails = os.getenv("ADMIN_EMAILS", "admin@example.com").split(",")
|
| 835 |
-
email_subject = f"New Order Received: {order_id}"
|
| 836 |
-
email_body = (
|
| 837 |
-
f"New Order Received:\n"
|
| 838 |
-
f"Order ID: {order_id}\n"
|
| 839 |
-
f"Dish(es): {dish_summary}\n"
|
| 840 |
-
f"Quantity: {quantity_total}\n"
|
| 841 |
-
f"Total Price: N{total_price}\n"
|
| 842 |
-
f"Phone: {state.data.get('phone_number', '')}\n"
|
| 843 |
-
f"Delivery Address: {state.data.get('address', 'Not Provided')}\n"
|
| 844 |
-
f"Extras: {state.data.get('extras', 'None')}\n"
|
| 845 |
-
f"Status: Pending Payment"
|
| 846 |
-
)
|
| 847 |
email_response = send_email_notification(order_details)
|
| 848 |
if email_response:
|
| 849 |
print("Email notification sent successfully.")
|
| 850 |
else:
|
| 851 |
print("Failed to send email notification.")
|
| 852 |
-
|
| 853 |
-
email_for_paystack = "customer@example.com" # Replace with
|
| 854 |
payment_data = create_paystack_payment_link(email_for_paystack, total_price * 100, order_id)
|
| 855 |
state.reset()
|
| 856 |
if user_id in user_state:
|
| 857 |
del user_state[user_id]
|
| 858 |
if payment_data.get("status"):
|
| 859 |
payment_link = payment_data["data"]["authorization_url"]
|
| 860 |
-
return (f"Thank you for your order of {quantity_total} serving(s) of {dish_summary}! "
|
| 861 |
f"Your Order ID is {order_id}.\n\n"
|
| 862 |
-
"Please complete your payment using
|
| 863 |
-
f"
|
| 864 |
-
"
|
| 865 |
-
" Account Number: 1433042821\n"
|
| 866 |
-
" Bank: Access Bank\n"
|
| 867 |
-
" Account Name: Angelo Food Court 2\n\n"
|
| 868 |
-
"If you choose the bank transfer option, please send a screenshot of your payment confirmation to this chatbot.\n\n"
|
| 869 |
-
"You can track your order status using your Order ID.\n"
|
| 870 |
-
"Is there anything else you'd like to order?")
|
| 871 |
else:
|
| 872 |
return (f"Your order has been placed with Order ID {order_id}, "
|
| 873 |
-
"but we could not initialize online payment. Please try again later
|
| 874 |
-
"you may opt to pay via bank transfer to Account Number 1433042821, Access Bank, Angelo Food Court 2 "
|
| 875 |
-
"and send your payment screenshot to this chatbot.")
|
| 876 |
elif message.lower() in ["no", "n"]:
|
| 877 |
state.reset()
|
| 878 |
if user_id in user_state:
|
|
@@ -882,19 +759,16 @@ async def process_order_flow(user_id: str, message: str) -> str:
|
|
| 882 |
return "I didn't understand that. Please type 'yes' to confirm your order or 'no' to cancel it."
|
| 883 |
|
| 884 |
return ""
|
| 885 |
-
|
| 886 |
|
| 887 |
def _parse_single_dish_line(message: str, dish_name: str) -> dict:
|
| 888 |
-
|
| 889 |
result = {"quantity": None, "phone": None, "address": None}
|
| 890 |
|
| 891 |
-
#
|
| 892 |
numbers = re.findall(r'\d+', message)
|
| 893 |
if numbers:
|
| 894 |
-
# We'll assume the first numeric is quantity if no "for" pattern is used
|
| 895 |
result["quantity"] = int(numbers[0])
|
| 896 |
|
| 897 |
-
#
|
| 898 |
phone_pattern = r'(\+?\d{10,15})'
|
| 899 |
phone_match = re.search(phone_pattern, message)
|
| 900 |
address = None
|
|
@@ -937,7 +811,7 @@ async def update_user_last_interaction(user_id: str):
|
|
| 937 |
await session.commit()
|
| 938 |
|
| 939 |
async def send_proactive_greeting(user_id: str):
|
| 940 |
-
greeting = "Hi again! We miss you. Would you like to see our new menu items or get personalized recommendations?"
|
| 941 |
await log_chat_to_db(user_id, "outbound", greeting)
|
| 942 |
return greeting
|
| 943 |
|
|
@@ -984,12 +858,12 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
|
|
| 984 |
"image_url": image_url
|
| 985 |
})
|
| 986 |
response_payload = {
|
| 987 |
-
"response": sentiment_modifier + "Here’s our
|
| 988 |
"menu": menu_with_images,
|
| 989 |
"follow_up": (
|
| 990 |
"To order, type the *number* or *name* of the dish you'd like. "
|
| 991 |
-
"For example, type '1' or '
|
| 992 |
-
"You can also ask for nutritional facts by typing, for example, 'Nutritional facts for
|
| 993 |
)
|
| 994 |
}
|
| 995 |
background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
|
|
@@ -1011,8 +885,7 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
|
|
| 1011 |
})
|
| 1012 |
return JSONResponse(content={"response": sentiment_modifier + order_response})
|
| 1013 |
|
| 1014 |
-
|
| 1015 |
-
default_response = "I'm sorry, I didn't understand that. Please type 'menu' to see our options or 'order' to place an order."
|
| 1016 |
background_tasks.add_task(log_chat_to_db, user_id, "outbound", default_response)
|
| 1017 |
conversation_context[user_id].append({
|
| 1018 |
"timestamp": datetime.utcnow().isoformat(),
|
|
@@ -1021,7 +894,6 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
|
|
| 1021 |
})
|
| 1022 |
return JSONResponse(content={"response": sentiment_modifier + default_response})
|
| 1023 |
|
| 1024 |
-
|
| 1025 |
@app.get("/chat_history/{user_id}")
|
| 1026 |
async def get_chat_history(user_id: str):
|
| 1027 |
async with async_session() as session:
|
|
@@ -1134,7 +1006,7 @@ async def payment_callback(request: Request):
|
|
| 1134 |
)
|
| 1135 |
except Exception as e:
|
| 1136 |
print(f"WhatsApp message sending failed: {e}")
|
| 1137 |
-
redirect_url = "https://
|
| 1138 |
return RedirectResponse(url=redirect_url)
|
| 1139 |
else:
|
| 1140 |
data = await request.json()
|
|
@@ -1163,10 +1035,8 @@ async def payment_callback(request: Request):
|
|
| 1163 |
else:
|
| 1164 |
raise HTTPException(status_code=404, detail="Order not found.")
|
| 1165 |
|
| 1166 |
-
|
| 1167 |
@app.get("/track_order/{order_id}")
|
| 1168 |
async def track_order(order_id: str):
|
| 1169 |
-
|
| 1170 |
async with async_session() as session:
|
| 1171 |
result = await session.execute(
|
| 1172 |
select(OrderTracking)
|
|
@@ -1185,4 +1055,3 @@ async def track_order(order_id: str):
|
|
| 1185 |
return JSONResponse(content=response)
|
| 1186 |
else:
|
| 1187 |
raise HTTPException(status_code=404, detail="No tracking information found for this order.")
|
| 1188 |
-
|
|
|
|
| 24 |
from sqlalchemy.orm import sessionmaker, declarative_base
|
| 25 |
from sqlalchemy import Column, Integer, String, DateTime, Text, Float
|
| 26 |
|
| 27 |
+
# Environment keys remain, but you can adjust them if needed.
|
| 28 |
SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "default_fallback_value")
|
| 29 |
PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "default_fallback_value")
|
| 30 |
DATABASE_URL = os.getenv("DATABASE_URL", "default_fallback_value")
|
|
|
|
| 36 |
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN", "default_value")
|
| 37 |
MANAGEMENT_WHATSAPP_NUMBER = os.getenv("MANAGEMENT_WHATSAPP_NUMBER", "default_value")
|
| 38 |
|
| 39 |
+
# Updated shipping costs for popular Los Angeles neighborhoods (in dollars)
|
| 40 |
TOWN_SHIPPING_COSTS = {
|
| 41 |
+
"downtown los angeles": 5,
|
| 42 |
+
"hollywood": 6,
|
| 43 |
+
"santa monica": 7,
|
| 44 |
+
"pasadena": 5,
|
| 45 |
+
"default": 8
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
Base = declarative_base()
|
|
|
|
| 100 |
id = Column(Integer, primary_key=True, index=True)
|
| 101 |
name = Column(String, unique=True, index=True)
|
| 102 |
description = Column(Text)
|
| 103 |
+
price = Column(Integer) # Price in dollars
|
| 104 |
nutrition = Column(Text)
|
| 105 |
|
| 106 |
class TownShippingCost(Base):
|
|
|
|
| 109 |
town = Column(String, unique=True, index=True)
|
| 110 |
cost = Column(Integer)
|
| 111 |
|
|
|
|
| 112 |
engine = create_async_engine(DATABASE_URL, echo=True)
|
| 113 |
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
| 114 |
|
|
|
|
| 120 |
conversation_context = {}
|
| 121 |
proactive_timer = {}
|
| 122 |
|
| 123 |
+
# --- Updated Menu for Creole Kings ---
|
| 124 |
menu_items = [
|
| 125 |
+
{"name": "Jambalaya", "description": "A classic Creole dish with rice, chicken, sausage, and shrimp", "price": 15, "nutrition": "Approximately 500 kcal"},
|
| 126 |
+
{"name": "Gumbo", "description": "A hearty stew with chicken, sausage, okra, and spices", "price": 18, "nutrition": "Approximately 600 kcal"},
|
| 127 |
+
{"name": "Crawfish Étouffée", "description": "A rich, flavorful dish with crawfish simmered in a spiced roux", "price": 20, "nutrition": "Approximately 550 kcal"},
|
| 128 |
+
{"name": "Red Beans & Rice", "description": "Slow-cooked red beans served over rice with Creole spices", "price": 12, "nutrition": "Approximately 450 kcal"},
|
| 129 |
+
{"name": "Shrimp Po' Boy", "description": "Crispy fried shrimp served on a French roll with lettuce and tomato", "price": 16, "nutrition": "Approximately 500 kcal"},
|
| 130 |
+
{"name": "Muffuletta Sandwich", "description": "A hearty sandwich with layered meats, cheeses, and olive salad", "price": 14, "nutrition": "Approximately 650 kcal"},
|
| 131 |
+
{"name": "Blackened Fish", "description": "Fish fillet seasoned with Cajun spices and seared", "price": 17, "nutrition": "Approximately 400 kcal"},
|
| 132 |
+
{"name": "Creole Salad", "description": "Fresh mixed greens with a Creole vinaigrette", "price": 10, "nutrition": "Approximately 300 kcal"},
|
| 133 |
+
{"name": "Beignets", "description": "Light and fluffy deep-fried pastries dusted with powdered sugar", "price": 8, "nutrition": "Approximately 350 kcal"},
|
| 134 |
+
{"name": "Bananas Foster", "description": "Sautéed bananas in a rich butter and brown sugar sauce, flambéed with rum", "price": 9, "nutrition": "Approximately 400 kcal"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
]
|
| 136 |
|
| 137 |
+
creole_drinks = [
|
| 138 |
+
{"name": "Hurricane Cocktail", "description": "A sweet and tangy cocktail with rum and passion fruit", "price": 12, "nutrition": "Approximately 250 kcal"},
|
| 139 |
+
{"name": "Cajun Spice Margarita", "description": "A margarita with a hint of Cajun spice", "price": 11, "nutrition": "Approximately 200 kcal"},
|
| 140 |
+
{"name": "Iced Tea", "description": "Refreshing iced tea, unsweetened or sweetened", "price": 5, "nutrition": "Approximately 100 kcal"},
|
| 141 |
+
{"name": "Lemonade", "description": "Freshly squeezed lemonade", "price": 6, "nutrition": "Approximately 120 kcal"}
|
|
|
|
|
|
|
| 142 |
]
|
| 143 |
|
| 144 |
+
menu_items.extend(creole_drinks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
SESSION_TIMEOUT = timedelta(minutes=5)
|
| 147 |
|
|
|
|
| 175 |
session.add(new_cost)
|
| 176 |
await session.commit()
|
| 177 |
|
|
|
|
| 178 |
async def log_chat_to_db(user_id: str, direction: str, message: str):
|
| 179 |
async with async_session() as session:
|
| 180 |
entry = ChatHistory(user_id=user_id, direction=direction, message=message)
|
|
|
|
| 217 |
"email": email,
|
| 218 |
"amount": amount,
|
| 219 |
"reference": reference,
|
| 220 |
+
"callback_url": "https://creolekings.com/payment_callback" # Updated callback URL example
|
| 221 |
}
|
| 222 |
try:
|
| 223 |
response = requests.post(url, json=data, headers=headers, timeout=10)
|
|
|
|
| 308 |
def calculate_eta(destination: str) -> str:
|
| 309 |
if not GOOGLE_MAPS_API_KEY:
|
| 310 |
return "ETA unavailable (Google Maps API key missing)."
|
| 311 |
+
# Updated origin address for Creole Kings in Los Angeles
|
| 312 |
+
origin = "123 Main Street, Los Angeles, CA"
|
| 313 |
url = f"https://maps.googleapis.com/maps/api/directions/json?origin={origin}&destination={destination}&key={GOOGLE_MAPS_API_KEY}"
|
| 314 |
|
| 315 |
try:
|
|
|
|
| 360 |
for item in items
|
| 361 |
]
|
| 362 |
|
|
|
|
| 363 |
async def track_order(user_id: str, order_id: str) -> str:
|
| 364 |
async with async_session() as session:
|
| 365 |
order_result = await session.execute(
|
|
|
|
| 436 |
Then, try an exact whole-word match using a regex with word boundaries.
|
| 437 |
If no exact match is found, fall back to fuzzy matching.
|
| 438 |
"""
|
|
|
|
| 439 |
normalized_input = re.sub(r'\s+', ' ', user_input.lower().strip())
|
| 440 |
matched_dishes = []
|
| 441 |
|
|
|
|
| 442 |
for item in menu_items:
|
| 443 |
dish_name = item["name"]
|
|
|
|
|
|
|
| 444 |
pattern = r'\b' + re.escape(dish_name.lower()) + r'\b'
|
| 445 |
if re.search(pattern, normalized_input):
|
| 446 |
matched_dishes.append(dish_name)
|
| 447 |
|
|
|
|
| 448 |
if matched_dishes:
|
| 449 |
return list(set(matched_dishes))
|
| 450 |
|
|
|
|
| 451 |
for item in menu_items:
|
| 452 |
dish_name = item["name"]
|
| 453 |
score = fuzz.token_sort_ratio(normalized_input, dish_name.lower())
|
|
|
|
| 456 |
|
| 457 |
return list(set(matched_dishes))
|
| 458 |
|
| 459 |
+
# Use the asynchronous get_dish_price from the database
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
async def get_dish_price(dish: str) -> int:
|
| 461 |
async with async_session() as session:
|
|
|
|
| 462 |
result = await session.execute(select(MenuItem).where(MenuItem.name.ilike(dish)))
|
| 463 |
menu_item = result.scalars().first()
|
| 464 |
return menu_item.price if menu_item else 0
|
| 465 |
|
|
|
|
| 466 |
async def get_shipping_cost(address: str) -> int:
|
| 467 |
async with async_session() as session:
|
| 468 |
result = await session.execute(select(TownShippingCost))
|
| 469 |
costs = result.scalars().all()
|
| 470 |
address_lower = address.lower()
|
|
|
|
| 471 |
for cost in costs:
|
| 472 |
if cost.town in address_lower:
|
| 473 |
return cost.cost
|
|
|
|
| 474 |
default_cost = next((c for c in costs if c.town == "default"), None)
|
| 475 |
+
return default_cost.cost if default_cost else 8
|
|
|
|
| 476 |
|
| 477 |
def send_email_notification(order_details):
|
| 478 |
+
# Updated email notification for Creole Kings
|
| 479 |
payload = {
|
| 480 |
+
"from": "orders@creolekings.com",
|
| 481 |
+
"to": "manager@creolekings.com",
|
| 482 |
"subject": f"New Order Received: {order_details['order_id']}",
|
| 483 |
"body": (
|
| 484 |
f"New Order Received:\n"
|
| 485 |
f"Order ID: {order_details['order_id']}\n"
|
| 486 |
f"Dish: {order_details['dish']}\n"
|
| 487 |
f"Quantity: {order_details['quantity']}\n"
|
| 488 |
+
f"Total Price: ${order_details['price']}\n"
|
| 489 |
f"Phone: {order_details.get('phone_number', 'Not Provided')}\n"
|
| 490 |
f"Delivery Address: {order_details.get('address', 'Not Provided')}\n"
|
| 491 |
f"Extras: {order_details.get('extras', 'None')}\n"
|
| 492 |
f"Status: Pending Payment"
|
| 493 |
),
|
| 494 |
+
"smtpHost": "smtp.gmail.com",
|
| 495 |
+
"smtpPort": 587,
|
| 496 |
+
"smtpSecure": "false",
|
| 497 |
+
"smtpUser": "orders@creolekings.com",
|
| 498 |
+
"smtpPassword": "creolekings_smtp_password",
|
|
|
|
| 499 |
}
|
| 500 |
|
|
|
|
| 501 |
url = "https://smtp-server-ten.vercel.app/smtp"
|
| 502 |
try:
|
| 503 |
response = requests.post(url, json=payload, timeout=10)
|
|
|
|
| 507 |
print(f"Error sending email: {e}")
|
| 508 |
return None
|
| 509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
async def process_order_flow(user_id: str, message: str) -> str:
|
| 511 |
"""
|
| 512 |
Processes order flow allowing an unlimited number of dishes.
|
|
|
|
| 517 |
del user_state[user_id]
|
| 518 |
state = None
|
| 519 |
|
| 520 |
+
# 1) Initialize order flow with a welcome message for Creole Kings.
|
| 521 |
if message.lower() in ["order", "menu"]:
|
| 522 |
state = ConversationState()
|
| 523 |
state.flow = "order"
|
| 524 |
state.step = 1
|
| 525 |
state.update_last_active()
|
| 526 |
user_state[user_id] = state
|
| 527 |
+
return "Welcome to Creole Kings! What dish would you like to order today?"
|
|
|
|
|
|
|
| 528 |
|
| 529 |
if not state and "order" in message.lower():
|
| 530 |
state = ConversationState()
|
|
|
|
| 532 |
state.step = 1
|
| 533 |
state.update_last_active()
|
| 534 |
user_state[user_id] = state
|
| 535 |
+
return "Welcome to Creole Kings! What dish would you like to order today?"
|
| 536 |
|
| 537 |
+
# 2) Detect dish(es)
|
| 538 |
if not state or state.flow != "order":
|
| 539 |
matched = match_dishes(message)
|
| 540 |
if matched:
|
| 541 |
if len(matched) == 1:
|
|
|
|
| 542 |
found_dish = matched[0]
|
| 543 |
state = ConversationState()
|
| 544 |
state.flow = "order"
|
|
|
|
| 564 |
state.step = 5
|
| 565 |
return (f"Thanks! Your phone number is recorded as: {state.data['phone_number']}.\n"
|
| 566 |
f"Your delivery address is: {state.data['address']}.\n"
|
| 567 |
+
f"Delivery fee: ${shipping_cost}. Would you like extras (yes/no)?")
|
| 568 |
elif state.data.get("phone_number") and not state.data.get("address"):
|
| 569 |
+
return "Thank you. Please provide your delivery address (e.g., '3105551234, 123 Main St, Los Angeles, CA')."
|
| 570 |
else:
|
| 571 |
+
return "Please provide your phone number and address (e.g., '3105551234, 123 Main St, Los Angeles, CA')."
|
| 572 |
else:
|
|
|
|
| 573 |
state = ConversationState()
|
| 574 |
state.flow = "order"
|
| 575 |
state.update_last_active()
|
|
|
|
| 582 |
else:
|
| 583 |
return "I couldn't identify the dish. Please type the dish name from our menu."
|
| 584 |
|
|
|
|
| 585 |
if state and state.flow == "order" and "candidate_dishes" in state.data:
|
| 586 |
normalized = message.strip().lower()
|
| 587 |
if normalized in ["both", "all"]:
|
|
|
|
| 590 |
state.step = 2
|
| 591 |
dishes_str = ", ".join(state.data["dishes"])
|
| 592 |
return (f"You have selected: {dishes_str}. How many servings of each would you like? "
|
| 593 |
+
"(For example, '2 for Jambalaya, 3 for Gumbo')")
|
| 594 |
else:
|
| 595 |
for dish in state.data["candidate_dishes"]:
|
| 596 |
if dish.lower() in normalized:
|
|
|
|
| 602 |
return (f"Please specify which one you'd like to order from: {dish_options} "
|
| 603 |
"(or type 'both' if you'd like to order all).")
|
| 604 |
|
| 605 |
+
# 3) Step 2: Parse quantity details.
|
|
|
|
|
|
|
| 606 |
if state and state.flow == "order" and state.step == 2:
|
|
|
|
| 607 |
if "dishes" in state.data and len(state.data["dishes"]) > 1:
|
| 608 |
pairs = re.findall(r'(\d+)\s*for\s*([a-zA-Z\s]+)', message, flags=re.IGNORECASE)
|
| 609 |
if not pairs:
|
| 610 |
return ("I'm sorry, I didn't understand the quantity details. "
|
| 611 |
+
"Please specify like '2 for Jambalaya, 3 for Gumbo'.")
|
| 612 |
order_quantities = {}
|
| 613 |
for quantity, dish_text in pairs:
|
| 614 |
dish_text = dish_text.strip().lower()
|
|
|
|
| 620 |
state.step = 3
|
| 621 |
summary = "\n".join([f"{q} serving(s) of {d}" for d, q in order_quantities.items()])
|
| 622 |
return (f"Got it. You have ordered:\n{summary}\n"
|
| 623 |
+
"Please provide your phone number and delivery address (e.g., '3105551234, 123 Main St, Los Angeles, CA').")
|
| 624 |
else:
|
| 625 |
return ("I'm sorry, I couldn't match those dishes. "
|
| 626 |
+
"Please try something like '2 for Jambalaya, 3 for Gumbo'.")
|
|
|
|
| 627 |
numbers = re.findall(r'\d+', message)
|
| 628 |
if not numbers:
|
| 629 |
return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
|
|
|
|
| 634 |
state.step = 3
|
| 635 |
dish = state.data.get("dish", "")
|
| 636 |
return (f"Got it. {quantity} serving(s) of {dish}.\n"
|
| 637 |
+
"Please provide your phone number and delivery address (e.g., '3105551234, 123 Main St, Los Angeles, CA').")
|
| 638 |
|
| 639 |
+
# 4) Step 3: Parse phone & address.
|
|
|
|
| 640 |
if state and state.flow == "order" and state.step == 3:
|
| 641 |
phone_pattern = r'(\+?\d{10,15})'
|
| 642 |
phone_match = re.search(phone_pattern, message)
|
|
|
|
| 653 |
state.data["address"] = message.strip()
|
| 654 |
else:
|
| 655 |
return ("Please provide both your phone number and address. "
|
| 656 |
+
"For example: '3105551234, 123 Main St, Los Angeles, CA'.")
|
| 657 |
if not state.data.get("address"):
|
| 658 |
return "Thank you. Please provide your delivery address."
|
|
|
|
| 659 |
shipping_cost = await get_shipping_cost(state.data["address"])
|
| 660 |
state.data["shipping_cost"] = shipping_cost
|
|
|
|
|
|
|
| 661 |
state.data["extras"] = ""
|
|
|
|
|
|
|
| 662 |
state.step = 7
|
|
|
|
|
|
|
| 663 |
if "orders" in state.data:
|
| 664 |
dish_summary = ", ".join(state.data["orders"].keys())
|
| 665 |
quantity_total = sum(state.data["orders"].values())
|
| 666 |
total_price = 0
|
| 667 |
for dish, qty in state.data["orders"].items():
|
| 668 |
+
dish_price = await get_dish_price(dish)
|
| 669 |
+
total_price += dish_price * qty
|
| 670 |
total_price += shipping_cost
|
| 671 |
else:
|
| 672 |
dish_summary = state.data.get("dish", "")
|
| 673 |
quantity_total = state.data.get("quantity", 1)
|
| 674 |
+
dish_price = await get_dish_price(dish_summary)
|
| 675 |
total_price = (quantity_total * dish_price) + shipping_cost
|
|
|
|
| 676 |
return (f"Order Summary:\n"
|
| 677 |
f"Dish(es): {dish_summary}\n"
|
| 678 |
f"Quantity: {quantity_total}\n"
|
| 679 |
f"Phone: {state.data.get('phone_number', '')}\n"
|
| 680 |
f"Address: {state.data.get('address', '')}\n"
|
| 681 |
+
f"Delivery Fee: ${shipping_cost}\n"
|
| 682 |
+
f"Total Price: ${total_price}\n"
|
| 683 |
"Confirm order? (yes/no)")
|
| 684 |
|
|
|
|
|
|
|
|
|
|
| 685 |
# 7) Step 7: Order Confirmation and Payment Link Generation
|
| 686 |
if state and state.flow == "order" and state.step == 7:
|
| 687 |
if message.lower() in ["yes", "y"]:
|
| 688 |
order_id = f"ORD-{int(time.time())}"
|
| 689 |
state.data["order_id"] = order_id
|
| 690 |
+
|
|
|
|
| 691 |
if "orders" in state.data:
|
| 692 |
total_price = 0
|
| 693 |
for dish, qty in state.data["orders"].items():
|
| 694 |
+
dish_price = await get_dish_price(dish)
|
| 695 |
+
total_price += dish_price * qty
|
| 696 |
total_price += state.data.get("shipping_cost", 0)
|
| 697 |
dish_summary = ", ".join(state.data["orders"].keys())
|
| 698 |
quantity_total = sum(state.data["orders"].values())
|
| 699 |
else:
|
| 700 |
dish_summary = state.data.get("dish", "")
|
| 701 |
quantity_total = state.data.get("quantity", 1)
|
| 702 |
+
dish_price = await get_dish_price(dish_summary)
|
| 703 |
total_price = (quantity_total * dish_price) + state.data.get("shipping_cost", 0)
|
| 704 |
|
| 705 |
state.data["price"] = str(total_price)
|
| 706 |
+
|
| 707 |
async def save_order():
|
| 708 |
async with async_session() as session:
|
| 709 |
order = Order(
|
|
|
|
| 719 |
await session.commit()
|
| 720 |
asyncio.create_task(save_order())
|
| 721 |
asyncio.create_task(log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment."))
|
| 722 |
+
|
|
|
|
| 723 |
order_details = {
|
| 724 |
"order_id": order_id,
|
| 725 |
"dish": dish_summary,
|
|
|
|
| 729 |
"address": state.data.get("address", "Not Provided"),
|
| 730 |
"extras": state.data.get("extras", "None")
|
| 731 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
email_response = send_email_notification(order_details)
|
| 733 |
if email_response:
|
| 734 |
print("Email notification sent successfully.")
|
| 735 |
else:
|
| 736 |
print("Failed to send email notification.")
|
| 737 |
+
|
| 738 |
+
email_for_paystack = "customer@example.com" # Replace with customer's email if available
|
| 739 |
payment_data = create_paystack_payment_link(email_for_paystack, total_price * 100, order_id)
|
| 740 |
state.reset()
|
| 741 |
if user_id in user_state:
|
| 742 |
del user_state[user_id]
|
| 743 |
if payment_data.get("status"):
|
| 744 |
payment_link = payment_data["data"]["authorization_url"]
|
| 745 |
+
return (f"Thank you for your order of {quantity_total} serving(s) of {dish_summary} from Creole Kings! "
|
| 746 |
f"Your Order ID is {order_id}.\n\n"
|
| 747 |
+
"Please complete your payment online using the link below:\n"
|
| 748 |
+
f"{payment_link}\n\n"
|
| 749 |
+
"You can track your order status using your Order ID.\nWould you like to place another order?")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
else:
|
| 751 |
return (f"Your order has been placed with Order ID {order_id}, "
|
| 752 |
+
"but we could not initialize online payment. Please try again later.")
|
|
|
|
|
|
|
| 753 |
elif message.lower() in ["no", "n"]:
|
| 754 |
state.reset()
|
| 755 |
if user_id in user_state:
|
|
|
|
| 759 |
return "I didn't understand that. Please type 'yes' to confirm your order or 'no' to cancel it."
|
| 760 |
|
| 761 |
return ""
|
|
|
|
| 762 |
|
| 763 |
def _parse_single_dish_line(message: str, dish_name: str) -> dict:
|
|
|
|
| 764 |
result = {"quantity": None, "phone": None, "address": None}
|
| 765 |
|
| 766 |
+
# Parse quantity
|
| 767 |
numbers = re.findall(r'\d+', message)
|
| 768 |
if numbers:
|
|
|
|
| 769 |
result["quantity"] = int(numbers[0])
|
| 770 |
|
| 771 |
+
# Parse phone
|
| 772 |
phone_pattern = r'(\+?\d{10,15})'
|
| 773 |
phone_match = re.search(phone_pattern, message)
|
| 774 |
address = None
|
|
|
|
| 811 |
await session.commit()
|
| 812 |
|
| 813 |
async def send_proactive_greeting(user_id: str):
|
| 814 |
+
greeting = "Hi again! We miss you at Creole Kings. Would you like to see our new menu items or get personalized recommendations?"
|
| 815 |
await log_chat_to_db(user_id, "outbound", greeting)
|
| 816 |
return greeting
|
| 817 |
|
|
|
|
| 858 |
"image_url": image_url
|
| 859 |
})
|
| 860 |
response_payload = {
|
| 861 |
+
"response": sentiment_modifier + "Here’s our mouthwatering Creole menu:",
|
| 862 |
"menu": menu_with_images,
|
| 863 |
"follow_up": (
|
| 864 |
"To order, type the *number* or *name* of the dish you'd like. "
|
| 865 |
+
"For example, type '1' or 'Jambalaya' to order.\n\n"
|
| 866 |
+
"You can also ask for nutritional facts by typing, for example, 'Nutritional facts for Gumbo'."
|
| 867 |
)
|
| 868 |
}
|
| 869 |
background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
|
|
|
|
| 885 |
})
|
| 886 |
return JSONResponse(content={"response": sentiment_modifier + order_response})
|
| 887 |
|
| 888 |
+
default_response = "I'm sorry, I didn't understand that. Please type 'menu' to see our options or 'order' to place an order at Creole Kings."
|
|
|
|
| 889 |
background_tasks.add_task(log_chat_to_db, user_id, "outbound", default_response)
|
| 890 |
conversation_context[user_id].append({
|
| 891 |
"timestamp": datetime.utcnow().isoformat(),
|
|
|
|
| 894 |
})
|
| 895 |
return JSONResponse(content={"response": sentiment_modifier + default_response})
|
| 896 |
|
|
|
|
| 897 |
@app.get("/chat_history/{user_id}")
|
| 898 |
async def get_chat_history(user_id: str):
|
| 899 |
async with async_session() as session:
|
|
|
|
| 1006 |
)
|
| 1007 |
except Exception as e:
|
| 1008 |
print(f"WhatsApp message sending failed: {e}")
|
| 1009 |
+
redirect_url = "https://creolekings.com/thankyou"
|
| 1010 |
return RedirectResponse(url=redirect_url)
|
| 1011 |
else:
|
| 1012 |
data = await request.json()
|
|
|
|
| 1035 |
else:
|
| 1036 |
raise HTTPException(status_code=404, detail="Order not found.")
|
| 1037 |
|
|
|
|
| 1038 |
@app.get("/track_order/{order_id}")
|
| 1039 |
async def track_order(order_id: str):
|
|
|
|
| 1040 |
async with async_session() as session:
|
| 1041 |
result = await session.execute(
|
| 1042 |
select(OrderTracking)
|
|
|
|
| 1055 |
return JSONResponse(content=response)
|
| 1056 |
else:
|
| 1057 |
raise HTTPException(status_code=404, detail="No tracking information found for this order.")
|
|
|