Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,6 +1,3 @@
|
|
1 |
-
# --------------------
|
2 |
-
# IMPORTS & SETUP
|
3 |
-
# --------------------
|
4 |
import re
|
5 |
import os
|
6 |
import time
|
@@ -11,23 +8,44 @@ from datetime import datetime, timedelta
|
|
11 |
from typing import Dict, List, Optional
|
12 |
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
|
13 |
from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
|
14 |
-
from
|
|
|
15 |
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
16 |
from sqlalchemy.orm import sessionmaker, declarative_base
|
17 |
from sqlalchemy import Column, Integer, String, DateTime, Text, Float, Boolean
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
# --------------------
|
20 |
-
# DATABASE
|
21 |
# --------------------
|
|
|
|
|
|
|
|
|
22 |
Base = declarative_base()
|
23 |
|
|
|
|
|
|
|
24 |
class UserProfile(Base):
|
25 |
__tablename__ = "user_profiles"
|
26 |
id = Column(Integer, primary_key=True, index=True)
|
27 |
user_id = Column(String(36), unique=True, index=True)
|
28 |
-
|
|
|
|
|
|
|
|
|
29 |
ux_mode = Column(String(20), default="default")
|
30 |
accessibility_prefs = Column(Text, default="{}")
|
|
|
31 |
|
32 |
class UXPreferences(Base):
|
33 |
__tablename__ = "ux_preferences"
|
@@ -36,6 +54,38 @@ class UXPreferences(Base):
|
|
36 |
color_scheme = Column(String(20), default="light")
|
37 |
input_preference = Column(String(20), default="buttons")
|
38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
# --------------------
|
40 |
# STATE MANAGEMENT
|
41 |
# --------------------
|
@@ -56,10 +106,13 @@ class ConversationState:
|
|
56 |
"role": role,
|
57 |
"message": message
|
58 |
})
|
59 |
-
# Keep only last 5 messages
|
60 |
if len(self.context) > 5:
|
61 |
self.context = self.context[-5:]
|
62 |
|
|
|
|
|
|
|
63 |
# --------------------
|
64 |
# CORE UTILITIES
|
65 |
# --------------------
|
@@ -97,7 +150,7 @@ async def get_or_create_user_profile(user_id: str, phone: str = None) -> UserPro
|
|
97 |
class DeliveryUXManager:
|
98 |
@staticmethod
|
99 |
async def generate_response_template(user_id: str) -> Dict:
|
100 |
-
"""Generate personalized response structure"""
|
101 |
profile = await get_or_create_user_profile(user_id)
|
102 |
return {
|
103 |
"meta": {
|
@@ -115,14 +168,14 @@ class DeliveryUXManager:
|
|
115 |
|
116 |
@staticmethod
|
117 |
async def handle_ux_preferences(user_id: str, preference: str):
|
118 |
-
"""Update UX preferences"""
|
119 |
async with async_session() as session:
|
120 |
prefs = await session.get(UXPreferences, user_id)
|
121 |
if not prefs:
|
122 |
prefs = UXPreferences(user_id=user_id)
|
123 |
session.add(prefs)
|
124 |
|
125 |
-
# Update specific preference
|
126 |
if preference.startswith("color_"):
|
127 |
prefs.color_scheme = preference.split("_")[1]
|
128 |
elif preference.startswith("speed_"):
|
@@ -130,28 +183,72 @@ class DeliveryUXManager:
|
|
130 |
|
131 |
await session.commit()
|
132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
# --------------------
|
134 |
# MAIN APPLICATION
|
135 |
# --------------------
|
136 |
app = FastAPI(title="Delivery Service Chatbot")
|
137 |
|
|
|
|
|
|
|
|
|
138 |
@app.post("/chatbot")
|
139 |
async def enhanced_chatbot_handler(request: Request, bg: BackgroundTasks):
|
140 |
data = await request.json()
|
141 |
user_id = data["user_id"]
|
142 |
message = data.get("message", "")
|
143 |
|
144 |
-
# Initialize state
|
145 |
if user_id not in user_state:
|
146 |
user_state[user_id] = ConversationState()
|
147 |
|
148 |
state = user_state[user_id]
|
149 |
state.update_last_active()
|
150 |
|
151 |
-
#
|
152 |
response = await DeliveryUXManager.generate_response_template(user_id)
|
|
|
|
|
153 |
|
154 |
-
#
|
|
|
155 |
|
156 |
return JSONResponse(response)
|
157 |
|
@@ -164,69 +261,34 @@ async def update_ux_preferences(request: Request):
|
|
164 |
)
|
165 |
return {"status": "preferences updated"}
|
166 |
|
167 |
-
# --------------------
|
168 |
-
# PROACTIVE FEATURES
|
169 |
-
# --------------------
|
170 |
-
async def send_proactive_update(user_id: str, update_type: str):
|
171 |
-
"""Send unsolicited updates to users"""
|
172 |
-
state = user_state.get(user_id)
|
173 |
-
if not state:
|
174 |
-
return # User not active
|
175 |
-
|
176 |
-
response = await DeliveryUXManager.generate_response_template(user_id)
|
177 |
-
|
178 |
-
if update_type == "delivery_eta":
|
179 |
-
response["content"]["text"] = "📦 Your package is arriving in 15 minutes!"
|
180 |
-
response["content"]["quick_replies"] = [
|
181 |
-
{"title": "Track Live", "action": "track_live"},
|
182 |
-
{"title": "Delay Delivery", "action": "delay_delivery"}
|
183 |
-
]
|
184 |
-
|
185 |
-
return response
|
186 |
-
|
187 |
-
# --------------------
|
188 |
-
# ERROR HANDLING
|
189 |
-
# --------------------
|
190 |
-
@app.exception_handler(HTTPException)
|
191 |
-
async def ux_error_handler(request: Request, exc: HTTPException):
|
192 |
-
return JSONResponse({
|
193 |
-
"meta": {"error": True},
|
194 |
-
"content": {
|
195 |
-
"text": f"⚠️ Error: {exc.detail}",
|
196 |
-
"quick_replies": [{"title": "Main Menu", "action": "reset"}]
|
197 |
-
}
|
198 |
-
}, status_code=exc.status_code)
|
199 |
-
|
200 |
-
if __name__ == "__main__":
|
201 |
-
import uvicorn
|
202 |
-
uvicorn.run(app, host="0.0.0.0", port=8000)
|
203 |
-
# --- FastAPI Setup & Endpoints ---
|
204 |
-
app = FastAPI()
|
205 |
-
|
206 |
-
@app.on_event("startup")
|
207 |
-
async def on_startup():
|
208 |
-
await init_db()
|
209 |
-
|
210 |
-
|
211 |
-
# --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
|
212 |
@app.get("/chat_history/{user_id}")
|
213 |
async def get_chat_history(user_id: str):
|
214 |
async with async_session() as session:
|
215 |
result = await session.execute(
|
216 |
-
|
217 |
)
|
218 |
-
history = result.
|
219 |
-
return [
|
|
|
|
|
|
|
|
|
220 |
|
221 |
@app.get("/order/{order_id}")
|
222 |
async def get_order(order_id: str):
|
223 |
async with async_session() as session:
|
224 |
result = await session.execute(
|
225 |
-
|
226 |
)
|
227 |
-
order = result.
|
228 |
if order:
|
229 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
230 |
else:
|
231 |
raise HTTPException(status_code=404, detail="Order not found.")
|
232 |
|
@@ -239,16 +301,23 @@ async def get_user_profile(user_id: str):
|
|
239 |
"name": profile.name,
|
240 |
"email": profile.email,
|
241 |
"preferences": profile.preferences,
|
242 |
-
"last_interaction": profile.last_interaction.isoformat()
|
|
|
|
|
243 |
}
|
244 |
|
245 |
@app.get("/analytics")
|
246 |
async def get_analytics():
|
247 |
async with async_session() as session:
|
248 |
-
msg_result = await session.execute(
|
|
|
|
|
249 |
total_messages = msg_result.scalar() or 0
|
250 |
-
order_result = await session.execute(
|
|
|
|
|
251 |
total_orders = order_result.scalar() or 0
|
|
|
252 |
sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
|
253 |
avg_sentiment = sentiment_result.scalar() or 0
|
254 |
return {
|
@@ -263,10 +332,11 @@ async def process_voice(file: UploadFile = File(...)):
|
|
263 |
simulated_text = "Simulated speech-to-text conversion result."
|
264 |
return {"transcription": simulated_text}
|
265 |
|
266 |
-
# Enhanced payment callback with security checks
|
267 |
@app.api_route("/payment_callback", methods=["GET", "POST"])
|
268 |
async def payment_callback(request: Request):
|
269 |
-
""
|
|
|
|
|
270 |
try:
|
271 |
if request.method == "POST":
|
272 |
# Verify Paystack signature
|
@@ -298,7 +368,6 @@ async def payment_callback(request: Request):
|
|
298 |
raise HTTPException(status_code=400, detail="Missing order reference")
|
299 |
|
300 |
async with async_session() as session:
|
301 |
-
# Get order with lock to prevent race conditions
|
302 |
result = await session.execute(
|
303 |
select(Order)
|
304 |
.where(Order.order_id == order_id)
|
@@ -326,9 +395,9 @@ async def payment_callback(request: Request):
|
|
326 |
|
327 |
await session.commit()
|
328 |
|
329 |
-
# Send notifications
|
330 |
await asyncio.gather(
|
331 |
-
log_order_tracking(order_id, "Payment Updated", status),
|
332 |
send_whatsapp_message(
|
333 |
MANAGEMENT_WHATSAPP_NUMBER,
|
334 |
f"Payment Update: Order {order_id} - {status}"
|
@@ -337,38 +406,35 @@ async def payment_callback(request: Request):
|
|
337 |
)
|
338 |
|
339 |
if request.method == "GET":
|
|
|
340 |
return RedirectResponse(
|
341 |
-
url=
|
342 |
status_code=303
|
343 |
)
|
344 |
return JSONResponse(content={"status": "success", "order_id": order_id})
|
345 |
|
346 |
except HTTPException as he:
|
347 |
-
raise
|
348 |
except Exception as e:
|
349 |
logger.error(f"Payment callback error: {str(e)}")
|
350 |
raise HTTPException(status_code=500, detail="Internal server error")
|
351 |
|
352 |
-
# Enhanced tracking endpoint with caching
|
353 |
from fastapi_cache.decorator import cache
|
354 |
|
355 |
@app.get("/track_order/{order_id}", response_model=List[dict])
|
356 |
@cache(expire=60) # Cache for 1 minute
|
357 |
-
async def get_order_tracking(order_id: str,
|
358 |
-
page: int = 1,
|
359 |
-
limit: int = 10):
|
360 |
"""
|
361 |
-
Get order tracking history with pagination
|
362 |
"""
|
363 |
try:
|
364 |
async with async_session() as session:
|
365 |
-
# Get order first to verify existence
|
366 |
order_result = await session.execute(
|
367 |
select(Order).where(Order.order_id == order_id)
|
|
|
368 |
if not order_result.scalar_one_or_none():
|
369 |
raise HTTPException(status_code=404, detail="Order not found")
|
370 |
|
371 |
-
# Get tracking history
|
372 |
tracking_result = await session.execute(
|
373 |
select(OrderTracking)
|
374 |
.where(OrderTracking.order_id == order_id)
|
@@ -387,40 +453,58 @@ async def get_order_tracking(order_id: str,
|
|
387 |
"status": update.status,
|
388 |
"message": update.message,
|
389 |
"timestamp": update.timestamp.isoformat(),
|
390 |
-
"location":
|
391 |
}
|
392 |
for update in tracking_updates
|
393 |
]
|
394 |
|
395 |
-
# Add estimated delivery time
|
396 |
order = await get_order_details(order_id)
|
397 |
-
if order.status == "shipped":
|
398 |
-
|
|
|
399 |
|
400 |
return JSONResponse(content=response)
|
401 |
|
402 |
except HTTPException as he:
|
403 |
-
raise
|
404 |
except Exception as e:
|
405 |
logger.error(f"Tracking error: {str(e)}")
|
406 |
raise HTTPException(status_code=500, detail="Internal server error")
|
407 |
|
408 |
-
#
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
414 |
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
|
|
|
|
|
|
|
|
423 |
|
424 |
-
|
425 |
-
|
426 |
-
|
|
|
|
|
|
|
|
|
1 |
import re
|
2 |
import os
|
3 |
import time
|
|
|
8 |
from typing import Dict, List, Optional
|
9 |
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
|
10 |
from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
|
11 |
+
from fastapi.encoders import jsonable_encoder
|
12 |
+
from sqlalchemy import select, update, func
|
13 |
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
14 |
from sqlalchemy.orm import sessionmaker, declarative_base
|
15 |
from sqlalchemy import Column, Integer, String, DateTime, Text, Float, Boolean
|
16 |
+
import logging
|
17 |
+
import hmac
|
18 |
+
import hashlib
|
19 |
+
import json
|
20 |
+
|
21 |
+
# Configure logging
|
22 |
+
logging.basicConfig(level=logging.INFO)
|
23 |
+
logger = logging.getLogger("delivery_service")
|
24 |
|
25 |
# --------------------
|
26 |
+
# DATABASE SETUP
|
27 |
# --------------------
|
28 |
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./test.db")
|
29 |
+
engine = create_async_engine(DATABASE_URL, echo=True)
|
30 |
+
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
31 |
+
|
32 |
Base = declarative_base()
|
33 |
|
34 |
+
# --------------------
|
35 |
+
# DATABASE MODELS
|
36 |
+
# --------------------
|
37 |
class UserProfile(Base):
|
38 |
__tablename__ = "user_profiles"
|
39 |
id = Column(Integer, primary_key=True, index=True)
|
40 |
user_id = Column(String(36), unique=True, index=True)
|
41 |
+
phone_number = Column(String(20), nullable=True)
|
42 |
+
name = Column(String(100), nullable=True)
|
43 |
+
email = Column(String(100), nullable=True)
|
44 |
+
preferences = Column(Text, default="{}")
|
45 |
+
last_interaction = Column(DateTime, default=datetime.utcnow)
|
46 |
ux_mode = Column(String(20), default="default")
|
47 |
accessibility_prefs = Column(Text, default="{}")
|
48 |
+
current_order_status = Column(String(50), default="None")
|
49 |
|
50 |
class UXPreferences(Base):
|
51 |
__tablename__ = "ux_preferences"
|
|
|
54 |
color_scheme = Column(String(20), default="light")
|
55 |
input_preference = Column(String(20), default="buttons")
|
56 |
|
57 |
+
class ChatHistory(Base):
|
58 |
+
__tablename__ = "chat_history"
|
59 |
+
id = Column(Integer, primary_key=True, index=True)
|
60 |
+
user_id = Column(String(36), index=True)
|
61 |
+
message = Column(Text)
|
62 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
63 |
+
|
64 |
+
class Order(Base):
|
65 |
+
__tablename__ = "orders"
|
66 |
+
order_id = Column(String(36), primary_key=True, index=True)
|
67 |
+
user_id = Column(String(36), index=True)
|
68 |
+
status = Column(String(20), default="pending")
|
69 |
+
payment_reference = Column(String(100), nullable=True)
|
70 |
+
last_location = Column(Text, nullable=True) # Stored as a JSON string
|
71 |
+
|
72 |
+
class OrderTracking(Base):
|
73 |
+
__tablename__ = "order_tracking"
|
74 |
+
id = Column(Integer, primary_key=True, index=True)
|
75 |
+
order_id = Column(String(36), index=True)
|
76 |
+
status = Column(String(50))
|
77 |
+
message = Column(Text)
|
78 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
79 |
+
location = Column(Text, nullable=True) # Stored as a JSON string
|
80 |
+
|
81 |
+
# --------------------
|
82 |
+
# INITIALIZATION
|
83 |
+
# --------------------
|
84 |
+
async def init_db():
|
85 |
+
async with engine.begin() as conn:
|
86 |
+
await conn.run_sync(Base.metadata.create_all)
|
87 |
+
logger.info("Database tables created.")
|
88 |
+
|
89 |
# --------------------
|
90 |
# STATE MANAGEMENT
|
91 |
# --------------------
|
|
|
106 |
"role": role,
|
107 |
"message": message
|
108 |
})
|
109 |
+
# Keep only the last 5 messages
|
110 |
if len(self.context) > 5:
|
111 |
self.context = self.context[-5:]
|
112 |
|
113 |
+
# Global state dictionary
|
114 |
+
user_state: Dict[str, ConversationState] = {}
|
115 |
+
|
116 |
# --------------------
|
117 |
# CORE UTILITIES
|
118 |
# --------------------
|
|
|
150 |
class DeliveryUXManager:
|
151 |
@staticmethod
|
152 |
async def generate_response_template(user_id: str) -> Dict:
|
153 |
+
"""Generate a personalized response structure."""
|
154 |
profile = await get_or_create_user_profile(user_id)
|
155 |
return {
|
156 |
"meta": {
|
|
|
168 |
|
169 |
@staticmethod
|
170 |
async def handle_ux_preferences(user_id: str, preference: str):
|
171 |
+
"""Update UX preferences."""
|
172 |
async with async_session() as session:
|
173 |
prefs = await session.get(UXPreferences, user_id)
|
174 |
if not prefs:
|
175 |
prefs = UXPreferences(user_id=user_id)
|
176 |
session.add(prefs)
|
177 |
|
178 |
+
# Update specific preference based on input
|
179 |
if preference.startswith("color_"):
|
180 |
prefs.color_scheme = preference.split("_")[1]
|
181 |
elif preference.startswith("speed_"):
|
|
|
183 |
|
184 |
await session.commit()
|
185 |
|
186 |
+
# --------------------
|
187 |
+
# HELPER FUNCTIONS FOR PAYMENT CALLBACK & TRACKING
|
188 |
+
# --------------------
|
189 |
+
async def log_order_tracking(order_id: str, status: str, message: str):
|
190 |
+
async with async_session() as session:
|
191 |
+
tracking_entry = OrderTracking(
|
192 |
+
order_id=order_id,
|
193 |
+
status=status,
|
194 |
+
message=message
|
195 |
+
)
|
196 |
+
session.add(tracking_entry)
|
197 |
+
await session.commit()
|
198 |
+
|
199 |
+
async def send_whatsapp_message(number: str, message: str):
|
200 |
+
# Dummy implementation for sending WhatsApp messages
|
201 |
+
logger.info(f"Sending WhatsApp message to {number}: {message}")
|
202 |
+
|
203 |
+
async def update_user_order_status(user_id: str, order_id: str, status: str):
|
204 |
+
async with async_session() as session:
|
205 |
+
await session.execute(
|
206 |
+
update(UserProfile)
|
207 |
+
.where(UserProfile.user_id == user_id)
|
208 |
+
.values(current_order_status=status)
|
209 |
+
)
|
210 |
+
await session.commit()
|
211 |
+
|
212 |
+
async def get_order_details(order_id: str) -> Optional[Order]:
|
213 |
+
async with async_session() as session:
|
214 |
+
result = await session.execute(
|
215 |
+
select(Order).where(Order.order_id == order_id)
|
216 |
+
)
|
217 |
+
return result.scalar_one_or_none()
|
218 |
+
|
219 |
+
def calculate_eta(last_location: dict) -> str:
|
220 |
+
# Implement actual ETA calculation logic here.
|
221 |
+
return "30 minutes"
|
222 |
+
|
223 |
# --------------------
|
224 |
# MAIN APPLICATION
|
225 |
# --------------------
|
226 |
app = FastAPI(title="Delivery Service Chatbot")
|
227 |
|
228 |
+
@app.on_event("startup")
|
229 |
+
async def on_startup():
|
230 |
+
await init_db()
|
231 |
+
|
232 |
@app.post("/chatbot")
|
233 |
async def enhanced_chatbot_handler(request: Request, bg: BackgroundTasks):
|
234 |
data = await request.json()
|
235 |
user_id = data["user_id"]
|
236 |
message = data.get("message", "")
|
237 |
|
238 |
+
# Initialize conversation state if it doesn't exist
|
239 |
if user_id not in user_state:
|
240 |
user_state[user_id] = ConversationState()
|
241 |
|
242 |
state = user_state[user_id]
|
243 |
state.update_last_active()
|
244 |
|
245 |
+
# Dummy conversation handling logic
|
246 |
response = await DeliveryUXManager.generate_response_template(user_id)
|
247 |
+
response["content"]["text"] = f"Received your message: {message}"
|
248 |
+
state.add_context("user", message)
|
249 |
|
250 |
+
# Background task to update user's last interaction in the DB
|
251 |
+
bg.add_task(update_user_last_interaction, user_id)
|
252 |
|
253 |
return JSONResponse(response)
|
254 |
|
|
|
261 |
)
|
262 |
return {"status": "preferences updated"}
|
263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
264 |
@app.get("/chat_history/{user_id}")
|
265 |
async def get_chat_history(user_id: str):
|
266 |
async with async_session() as session:
|
267 |
result = await session.execute(
|
268 |
+
select(ChatHistory).where(ChatHistory.user_id == user_id)
|
269 |
)
|
270 |
+
history = result.scalars().all()
|
271 |
+
return [jsonable_encoder({
|
272 |
+
"user_id": entry.user_id,
|
273 |
+
"message": entry.message,
|
274 |
+
"timestamp": entry.timestamp.isoformat()
|
275 |
+
}) for entry in history]
|
276 |
|
277 |
@app.get("/order/{order_id}")
|
278 |
async def get_order(order_id: str):
|
279 |
async with async_session() as session:
|
280 |
result = await session.execute(
|
281 |
+
select(Order).where(Order.order_id == order_id)
|
282 |
)
|
283 |
+
order = result.scalar_one_or_none()
|
284 |
if order:
|
285 |
+
return jsonable_encoder({
|
286 |
+
"order_id": order.order_id,
|
287 |
+
"user_id": order.user_id,
|
288 |
+
"status": order.status,
|
289 |
+
"payment_reference": order.payment_reference,
|
290 |
+
"last_location": order.last_location
|
291 |
+
})
|
292 |
else:
|
293 |
raise HTTPException(status_code=404, detail="Order not found.")
|
294 |
|
|
|
301 |
"name": profile.name,
|
302 |
"email": profile.email,
|
303 |
"preferences": profile.preferences,
|
304 |
+
"last_interaction": profile.last_interaction.isoformat(),
|
305 |
+
"ux_mode": profile.ux_mode,
|
306 |
+
"current_order_status": profile.current_order_status
|
307 |
}
|
308 |
|
309 |
@app.get("/analytics")
|
310 |
async def get_analytics():
|
311 |
async with async_session() as session:
|
312 |
+
msg_result = await session.execute(
|
313 |
+
select(func.count()).select_from(ChatHistory)
|
314 |
+
)
|
315 |
total_messages = msg_result.scalar() or 0
|
316 |
+
order_result = await session.execute(
|
317 |
+
select(func.count()).select_from(Order)
|
318 |
+
)
|
319 |
total_orders = order_result.scalar() or 0
|
320 |
+
# This query assumes a sentiment_logs table exists with a sentiment_score column.
|
321 |
sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
|
322 |
avg_sentiment = sentiment_result.scalar() or 0
|
323 |
return {
|
|
|
332 |
simulated_text = "Simulated speech-to-text conversion result."
|
333 |
return {"transcription": simulated_text}
|
334 |
|
|
|
335 |
@app.api_route("/payment_callback", methods=["GET", "POST"])
|
336 |
async def payment_callback(request: Request):
|
337 |
+
PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "dummy_secret")
|
338 |
+
MANAGEMENT_WHATSAPP_NUMBER = os.getenv("MANAGEMENT_WHATSAPP_NUMBER", "+1234567890")
|
339 |
+
|
340 |
try:
|
341 |
if request.method == "POST":
|
342 |
# Verify Paystack signature
|
|
|
368 |
raise HTTPException(status_code=400, detail="Missing order reference")
|
369 |
|
370 |
async with async_session() as session:
|
|
|
371 |
result = await session.execute(
|
372 |
select(Order)
|
373 |
.where(Order.order_id == order_id)
|
|
|
395 |
|
396 |
await session.commit()
|
397 |
|
398 |
+
# Send notifications concurrently
|
399 |
await asyncio.gather(
|
400 |
+
log_order_tracking(order_id, "Payment Updated", f"Payment status changed to {status}"),
|
401 |
send_whatsapp_message(
|
402 |
MANAGEMENT_WHATSAPP_NUMBER,
|
403 |
f"Payment Update: Order {order_id} - {status}"
|
|
|
406 |
)
|
407 |
|
408 |
if request.method == "GET":
|
409 |
+
redirect_url = os.getenv("PAYMENT_REDIRECT_URL", "https://default-redirect.com")
|
410 |
return RedirectResponse(
|
411 |
+
url=redirect_url,
|
412 |
status_code=303
|
413 |
)
|
414 |
return JSONResponse(content={"status": "success", "order_id": order_id})
|
415 |
|
416 |
except HTTPException as he:
|
417 |
+
raise he
|
418 |
except Exception as e:
|
419 |
logger.error(f"Payment callback error: {str(e)}")
|
420 |
raise HTTPException(status_code=500, detail="Internal server error")
|
421 |
|
|
|
422 |
from fastapi_cache.decorator import cache
|
423 |
|
424 |
@app.get("/track_order/{order_id}", response_model=List[dict])
|
425 |
@cache(expire=60) # Cache for 1 minute
|
426 |
+
async def get_order_tracking(order_id: str, page: int = 1, limit: int = 10):
|
|
|
|
|
427 |
"""
|
428 |
+
Get order tracking history with pagination.
|
429 |
"""
|
430 |
try:
|
431 |
async with async_session() as session:
|
|
|
432 |
order_result = await session.execute(
|
433 |
select(Order).where(Order.order_id == order_id)
|
434 |
+
)
|
435 |
if not order_result.scalar_one_or_none():
|
436 |
raise HTTPException(status_code=404, detail="Order not found")
|
437 |
|
|
|
438 |
tracking_result = await session.execute(
|
439 |
select(OrderTracking)
|
440 |
.where(OrderTracking.order_id == order_id)
|
|
|
453 |
"status": update.status,
|
454 |
"message": update.message,
|
455 |
"timestamp": update.timestamp.isoformat(),
|
456 |
+
"location": json.loads(update.location) if update.location else None
|
457 |
}
|
458 |
for update in tracking_updates
|
459 |
]
|
460 |
|
461 |
+
# Add estimated delivery time if order is shipped
|
462 |
order = await get_order_details(order_id)
|
463 |
+
if order and order.status.lower() == "shipped":
|
464 |
+
loc = json.loads(order.last_location) if order.last_location else {}
|
465 |
+
response[0]["estimated_delivery"] = calculate_eta(loc)
|
466 |
|
467 |
return JSONResponse(content=response)
|
468 |
|
469 |
except HTTPException as he:
|
470 |
+
raise he
|
471 |
except Exception as e:
|
472 |
logger.error(f"Tracking error: {str(e)}")
|
473 |
raise HTTPException(status_code=500, detail="Internal server error")
|
474 |
|
475 |
+
# --------------------
|
476 |
+
# PROACTIVE FEATURES
|
477 |
+
# --------------------
|
478 |
+
async def send_proactive_update(user_id: str, update_type: str):
|
479 |
+
"""Send unsolicited updates to users."""
|
480 |
+
state = user_state.get(user_id)
|
481 |
+
if not state:
|
482 |
+
return # User not active
|
483 |
+
|
484 |
+
response = await DeliveryUXManager.generate_response_template(user_id)
|
485 |
+
|
486 |
+
if update_type == "delivery_eta":
|
487 |
+
response["content"]["text"] = "📦 Your package is arriving in 15 minutes!"
|
488 |
+
response["content"]["quick_replies"] = [
|
489 |
+
{"title": "Track Live", "action": "track_live"},
|
490 |
+
{"title": "Delay Delivery", "action": "delay_delivery"}
|
491 |
+
]
|
492 |
+
|
493 |
+
return response
|
494 |
|
495 |
+
# --------------------
|
496 |
+
# ERROR HANDLING
|
497 |
+
# --------------------
|
498 |
+
@app.exception_handler(HTTPException)
|
499 |
+
async def ux_error_handler(request: Request, exc: HTTPException):
|
500 |
+
return JSONResponse({
|
501 |
+
"meta": {"error": True},
|
502 |
+
"content": {
|
503 |
+
"text": f"⚠️ Error: {exc.detail}",
|
504 |
+
"quick_replies": [{"title": "Main Menu", "action": "reset"}]
|
505 |
+
}
|
506 |
+
}, status_code=exc.status_code)
|
507 |
|
508 |
+
if __name__ == "__main__":
|
509 |
+
import uvicorn
|
510 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|