Commit
Β·
1624f02
1
Parent(s):
5b558e5
feat: Add mandatory expected delivery time and SLA tracking to order management
Browse files- Implemented expected_delivery_time as a required field when creating orders.
- Added delivery_status and sla_grace_period_minutes to track delivery performance.
- Updated relevant functions and tests to ensure proper handling of new fields.
- Enhanced validation for expected delivery time to ensure it is in the future.
- MCP_TOOLS_SUMMARY.md +12 -2
- chat/tools.py +150 -39
- database/migrations/006_add_delivery_timing.py +106 -0
- server.py +18 -4
- test_delivery_timing.py +273 -0
- test_driver_validation.py +187 -0
MCP_TOOLS_SUMMARY.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
| 8 |
|
| 9 |
## Order Management (8 tools)
|
| 10 |
|
| 11 |
-
4. **`create_order`** - Create new delivery order
|
| 12 |
5. **`count_orders`** - Count orders by status (pending/assigned/in_transit/delivered)
|
| 13 |
6. **`fetch_orders`** - Get list of orders with filters and pagination
|
| 14 |
-
7. **`get_order_details`** - Get full details of specific order by ID
|
| 15 |
8. **`search_orders`** - Search orders by customer name, address, or order ID
|
| 16 |
9. **`get_incomplete_orders`** - Get all pending/assigned/in_transit orders
|
| 17 |
10. **`update_order`** - Update order status, driver, location, notes (with assignment cascading)
|
|
@@ -58,6 +58,9 @@
|
|
| 58 |
- β
Toll detection & avoidance
|
| 59 |
- β
Complete fleet management (orders + drivers + assignments)
|
| 60 |
- β
Assignment system with automatic route calculation
|
|
|
|
|
|
|
|
|
|
| 61 |
- β
**Automatic driver location updates on delivery completion**
|
| 62 |
- β
**Mandatory location + reason tracking for failed deliveries**
|
| 63 |
- β
**Structured failure reasons for analytics and reporting**
|
|
@@ -71,7 +74,14 @@
|
|
| 71 |
- **Manual assignment** with validation (pending orders + active drivers only)
|
| 72 |
- **Automatic route calculation** from driver location to delivery address
|
| 73 |
- **Delivery completion** with automatic driver location update to delivery address
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
- **Delivery failure handling** with mandatory GPS location and failure reason
|
|
|
|
| 75 |
- **Structured failure reasons**: customer_not_available, wrong_address, refused_delivery, damaged_goods, payment_issue, vehicle_breakdown, access_restricted, weather_conditions, other
|
| 76 |
- **Status management** with cascading updates across orders/drivers/assignments
|
| 77 |
- **Safety checks** preventing deletion of orders/drivers with active assignments
|
|
|
|
| 8 |
|
| 9 |
## Order Management (8 tools)
|
| 10 |
|
| 11 |
+
4. **`create_order`** - Create new delivery order with **MANDATORY expected_delivery_time** and SLA tracking
|
| 12 |
5. **`count_orders`** - Count orders by status (pending/assigned/in_transit/delivered)
|
| 13 |
6. **`fetch_orders`** - Get list of orders with filters and pagination
|
| 14 |
+
7. **`get_order_details`** - Get full details of specific order by ID including timing and SLA data
|
| 15 |
8. **`search_orders`** - Search orders by customer name, address, or order ID
|
| 16 |
9. **`get_incomplete_orders`** - Get all pending/assigned/in_transit orders
|
| 17 |
10. **`update_order`** - Update order status, driver, location, notes (with assignment cascading)
|
|
|
|
| 58 |
- β
Toll detection & avoidance
|
| 59 |
- β
Complete fleet management (orders + drivers + assignments)
|
| 60 |
- β
Assignment system with automatic route calculation
|
| 61 |
+
- β
**Mandatory delivery deadline (expected_delivery_time) when creating orders**
|
| 62 |
+
- β
**Automatic SLA tracking with grace period**
|
| 63 |
+
- β
**Delivery performance status: on_time, late, very_late, failed_on_time, failed_late**
|
| 64 |
- β
**Automatic driver location updates on delivery completion**
|
| 65 |
- β
**Mandatory location + reason tracking for failed deliveries**
|
| 66 |
- β
**Structured failure reasons for analytics and reporting**
|
|
|
|
| 74 |
- **Manual assignment** with validation (pending orders + active drivers only)
|
| 75 |
- **Automatic route calculation** from driver location to delivery address
|
| 76 |
- **Delivery completion** with automatic driver location update to delivery address
|
| 77 |
+
- **SLA & Timing Tracking**:
|
| 78 |
+
- Mandatory `expected_delivery_time` when creating orders
|
| 79 |
+
- Automatic comparison of actual vs expected delivery time
|
| 80 |
+
- Grace period support (default: 15 minutes)
|
| 81 |
+
- Performance statuses: `on_time`, `late` (within grace), `very_late` (SLA violation)
|
| 82 |
+
- `delivered_at` field automatically populated on completion/failure
|
| 83 |
- **Delivery failure handling** with mandatory GPS location and failure reason
|
| 84 |
+
- **Failure timing tracking**: `failed_on_time` vs `failed_late` status
|
| 85 |
- **Structured failure reasons**: customer_not_available, wrong_address, refused_delivery, damaged_goods, payment_issue, vehicle_breakdown, access_restricted, weather_conditions, other
|
| 86 |
- **Status management** with cascading updates across orders/drivers/assignments
|
| 87 |
- **Safety checks** preventing deletion of orders/drivers with active assignments
|
chat/tools.py
CHANGED
|
@@ -1332,7 +1332,7 @@ def handle_create_order(tool_input: dict) -> dict:
|
|
| 1332 |
Execute order creation tool
|
| 1333 |
|
| 1334 |
Args:
|
| 1335 |
-
tool_input: Dict with order fields
|
| 1336 |
|
| 1337 |
Returns:
|
| 1338 |
Order creation result
|
|
@@ -1344,30 +1344,48 @@ def handle_create_order(tool_input: dict) -> dict:
|
|
| 1344 |
delivery_address = tool_input.get("delivery_address")
|
| 1345 |
delivery_lat = tool_input.get("delivery_lat")
|
| 1346 |
delivery_lng = tool_input.get("delivery_lng")
|
|
|
|
| 1347 |
priority = tool_input.get("priority", "standard")
|
| 1348 |
special_instructions = tool_input.get("special_instructions")
|
| 1349 |
weight_kg = tool_input.get("weight_kg", 5.0)
|
|
|
|
| 1350 |
|
| 1351 |
-
# Validate required fields
|
| 1352 |
-
if not all([customer_name, delivery_address, delivery_lat, delivery_lng]):
|
| 1353 |
return {
|
| 1354 |
"success": False,
|
| 1355 |
-
"error": "Missing required fields: customer_name, delivery_address, delivery_lat, delivery_lng"
|
| 1356 |
}
|
| 1357 |
|
| 1358 |
# Generate order ID with microseconds to prevent collisions
|
| 1359 |
now = datetime.now()
|
| 1360 |
order_id = f"ORD-{now.strftime('%Y%m%d%H%M%S%f')[:18]}" # YYYYMMDDHHMMSSΞΌΞΌΞΌΞΌΞΌΞΌ (18 chars)
|
| 1361 |
|
| 1362 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1363 |
time_window_end_str = tool_input.get("time_window_end")
|
| 1364 |
if time_window_end_str:
|
| 1365 |
try:
|
| 1366 |
time_window_end = datetime.fromisoformat(time_window_end_str.replace('Z', '+00:00'))
|
| 1367 |
except:
|
| 1368 |
-
time_window_end =
|
| 1369 |
else:
|
| 1370 |
-
time_window_end =
|
| 1371 |
|
| 1372 |
time_window_start = now + timedelta(hours=2)
|
| 1373 |
|
|
@@ -1376,9 +1394,9 @@ def handle_create_order(tool_input: dict) -> dict:
|
|
| 1376 |
INSERT INTO orders (
|
| 1377 |
order_id, customer_name, customer_phone, customer_email,
|
| 1378 |
delivery_address, delivery_lat, delivery_lng,
|
| 1379 |
-
time_window_start, time_window_end,
|
| 1380 |
-
priority, weight_kg, status, special_instructions
|
| 1381 |
-
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
| 1382 |
"""
|
| 1383 |
|
| 1384 |
params = (
|
|
@@ -1391,15 +1409,17 @@ def handle_create_order(tool_input: dict) -> dict:
|
|
| 1391 |
delivery_lng,
|
| 1392 |
time_window_start,
|
| 1393 |
time_window_end,
|
|
|
|
| 1394 |
priority,
|
| 1395 |
weight_kg,
|
| 1396 |
"pending",
|
| 1397 |
-
special_instructions
|
|
|
|
| 1398 |
)
|
| 1399 |
|
| 1400 |
try:
|
| 1401 |
execute_write(query, params)
|
| 1402 |
-
logger.info(f"Order created: {order_id}")
|
| 1403 |
|
| 1404 |
return {
|
| 1405 |
"success": True,
|
|
@@ -1407,9 +1427,10 @@ def handle_create_order(tool_input: dict) -> dict:
|
|
| 1407 |
"status": "pending",
|
| 1408 |
"customer": customer_name,
|
| 1409 |
"address": delivery_address,
|
| 1410 |
-
"
|
|
|
|
| 1411 |
"priority": priority,
|
| 1412 |
-
"message": f"Order {order_id} created successfully!"
|
| 1413 |
}
|
| 1414 |
except Exception as e:
|
| 1415 |
logger.error(f"Database error creating order: {e}")
|
|
@@ -1433,10 +1454,12 @@ def handle_create_driver(tool_input: dict) -> dict:
|
|
| 1433 |
name = tool_input.get("name")
|
| 1434 |
phone = tool_input.get("phone")
|
| 1435 |
email = tool_input.get("email")
|
| 1436 |
-
vehicle_type = tool_input.get("vehicle_type"
|
| 1437 |
vehicle_plate = tool_input.get("vehicle_plate")
|
| 1438 |
capacity_kg = tool_input.get("capacity_kg", 1000.0)
|
| 1439 |
capacity_m3 = tool_input.get("capacity_m3", 12.0)
|
|
|
|
|
|
|
| 1440 |
|
| 1441 |
# Convert skills to regular list (handles protobuf RepeatedComposite)
|
| 1442 |
skills_raw = tool_input.get("skills", [])
|
|
@@ -1444,21 +1467,40 @@ def handle_create_driver(tool_input: dict) -> dict:
|
|
| 1444 |
|
| 1445 |
status = tool_input.get("status", "active")
|
| 1446 |
|
| 1447 |
-
# Validate required fields
|
| 1448 |
-
if not name:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1449 |
return {
|
| 1450 |
"success": False,
|
| 1451 |
-
"error": "
|
| 1452 |
}
|
| 1453 |
|
| 1454 |
# Generate driver ID with microseconds to prevent collisions
|
| 1455 |
now = datetime.now()
|
| 1456 |
driver_id = f"DRV-{now.strftime('%Y%m%d%H%M%S%f')[:18]}" # YYYYMMDDHHMMSSΞΌΞΌΞΌΞΌΞΌΞΌ (18 chars)
|
| 1457 |
|
| 1458 |
-
# Default location (San Francisco)
|
| 1459 |
-
current_lat = tool_input.get("current_lat", 37.7749)
|
| 1460 |
-
current_lng = tool_input.get("current_lng", -122.4194)
|
| 1461 |
-
|
| 1462 |
# Insert into database
|
| 1463 |
query = """
|
| 1464 |
INSERT INTO drivers (
|
|
@@ -2478,10 +2520,10 @@ def handle_get_order_details(tool_input: dict) -> dict:
|
|
| 2478 |
order_id, customer_name, customer_phone, customer_email,
|
| 2479 |
pickup_address, pickup_lat, pickup_lng,
|
| 2480 |
delivery_address, delivery_lat, delivery_lng,
|
| 2481 |
-
time_window_start, time_window_end,
|
| 2482 |
priority, weight_kg, volume_m3, special_instructions,
|
| 2483 |
-
status, assigned_driver_id,
|
| 2484 |
-
created_at, updated_at, delivered_at,
|
| 2485 |
order_value, payment_status,
|
| 2486 |
requires_signature, is_fragile, requires_cold_storage
|
| 2487 |
FROM orders
|
|
@@ -2527,6 +2569,12 @@ def handle_get_order_details(tool_input: dict) -> dict:
|
|
| 2527 |
"volume_m3": float(row['volume_m3']) if row['volume_m3'] else None,
|
| 2528 |
"special_instructions": row['special_instructions']
|
| 2529 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2530 |
"flags": {
|
| 2531 |
"requires_signature": row['requires_signature'],
|
| 2532 |
"is_fragile": row['is_fragile'],
|
|
@@ -2539,8 +2587,7 @@ def handle_get_order_details(tool_input: dict) -> dict:
|
|
| 2539 |
"assigned_driver_id": row['assigned_driver_id'],
|
| 2540 |
"timestamps": {
|
| 2541 |
"created_at": str(row['created_at']),
|
| 2542 |
-
"updated_at": str(row['updated_at']) if row['updated_at'] else None
|
| 2543 |
-
"delivered_at": str(row['delivered_at']) if row['delivered_at'] else None
|
| 2544 |
}
|
| 2545 |
}
|
| 2546 |
|
|
@@ -3918,12 +3965,12 @@ def handle_complete_delivery(tool_input: dict) -> dict:
|
|
| 3918 |
conn = get_db_connection()
|
| 3919 |
cursor = conn.cursor()
|
| 3920 |
|
| 3921 |
-
# Get assignment details
|
| 3922 |
cursor.execute("""
|
| 3923 |
SELECT
|
| 3924 |
a.status, a.order_id, a.driver_id,
|
| 3925 |
a.delivery_location_lat, a.delivery_location_lng, a.delivery_address,
|
| 3926 |
-
o.customer_name,
|
| 3927 |
d.name as driver_name
|
| 3928 |
FROM assignments a
|
| 3929 |
JOIN orders o ON a.order_id = o.order_id
|
|
@@ -3948,6 +3995,8 @@ def handle_complete_delivery(tool_input: dict) -> dict:
|
|
| 3948 |
delivery_address = assignment_row['delivery_address']
|
| 3949 |
customer_name = assignment_row['customer_name']
|
| 3950 |
driver_name = assignment_row['driver_name']
|
|
|
|
|
|
|
| 3951 |
|
| 3952 |
# Validate status
|
| 3953 |
if status not in ["active", "in_progress"]:
|
|
@@ -4002,12 +4051,45 @@ def handle_complete_delivery(tool_input: dict) -> dict:
|
|
| 4002 |
|
| 4003 |
logger.info(f"Driver {driver_id} location updated to delivery address: ({delivery_lat}, {delivery_lng})")
|
| 4004 |
|
| 4005 |
-
# Step 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4006 |
cursor.execute("""
|
| 4007 |
UPDATE orders
|
| 4008 |
-
SET status = 'delivered',
|
|
|
|
|
|
|
|
|
|
| 4009 |
WHERE order_id = %s
|
| 4010 |
-
""", (completion_time, order_id))
|
|
|
|
|
|
|
| 4011 |
|
| 4012 |
# Step 4: Check if driver has other active assignments
|
| 4013 |
cursor.execute("""
|
|
@@ -4044,6 +4126,8 @@ def handle_complete_delivery(tool_input: dict) -> dict:
|
|
| 4044 |
"customer_name": customer_name,
|
| 4045 |
"driver_name": driver_name,
|
| 4046 |
"completed_at": completion_time.isoformat(),
|
|
|
|
|
|
|
| 4047 |
"delivery_location": {
|
| 4048 |
"lat": float(delivery_lat),
|
| 4049 |
"lng": float(delivery_lng),
|
|
@@ -4054,7 +4138,7 @@ def handle_complete_delivery(tool_input: dict) -> dict:
|
|
| 4054 |
"location_updated_at": completion_time.isoformat()
|
| 4055 |
},
|
| 4056 |
"cascading_actions": cascading_actions,
|
| 4057 |
-
"message": f"Delivery completed! Order {order_id} delivered by {driver_name}. Driver location updated to delivery address."
|
| 4058 |
}
|
| 4059 |
|
| 4060 |
except Exception as e:
|
|
@@ -4153,12 +4237,12 @@ def handle_fail_delivery(tool_input: dict) -> dict:
|
|
| 4153 |
conn = get_db_connection()
|
| 4154 |
cursor = conn.cursor()
|
| 4155 |
|
| 4156 |
-
# Get assignment details
|
| 4157 |
cursor.execute("""
|
| 4158 |
SELECT
|
| 4159 |
a.status, a.order_id, a.driver_id,
|
| 4160 |
a.delivery_address,
|
| 4161 |
-
o.customer_name,
|
| 4162 |
d.name as driver_name
|
| 4163 |
FROM assignments a
|
| 4164 |
JOIN orders o ON a.order_id = o.order_id
|
|
@@ -4181,6 +4265,8 @@ def handle_fail_delivery(tool_input: dict) -> dict:
|
|
| 4181 |
delivery_address = assignment_row['delivery_address']
|
| 4182 |
customer_name = assignment_row['customer_name']
|
| 4183 |
driver_name = assignment_row['driver_name']
|
|
|
|
|
|
|
| 4184 |
|
| 4185 |
# Validate status
|
| 4186 |
if status not in ["active", "in_progress"]:
|
|
@@ -4227,12 +4313,35 @@ def handle_fail_delivery(tool_input: dict) -> dict:
|
|
| 4227 |
|
| 4228 |
logger.info(f"Driver {driver_id} location updated to reported position: ({current_lat}, {current_lng})")
|
| 4229 |
|
| 4230 |
-
# Step 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4231 |
cursor.execute("""
|
| 4232 |
UPDATE orders
|
| 4233 |
-
SET status = 'failed',
|
|
|
|
|
|
|
|
|
|
| 4234 |
WHERE order_id = %s
|
| 4235 |
-
""", (failure_time, order_id))
|
|
|
|
|
|
|
| 4236 |
|
| 4237 |
# Step 4: Check if driver has other active assignments
|
| 4238 |
cursor.execute("""
|
|
@@ -4274,6 +4383,8 @@ def handle_fail_delivery(tool_input: dict) -> dict:
|
|
| 4274 |
"failed_at": failure_time.isoformat(),
|
| 4275 |
"failure_reason": failure_reason,
|
| 4276 |
"failure_reason_display": reason_display,
|
|
|
|
|
|
|
| 4277 |
"delivery_address": delivery_address,
|
| 4278 |
"driver_location": {
|
| 4279 |
"lat": current_lat,
|
|
@@ -4281,7 +4392,7 @@ def handle_fail_delivery(tool_input: dict) -> dict:
|
|
| 4281 |
"updated_at": failure_time.isoformat()
|
| 4282 |
},
|
| 4283 |
"cascading_actions": cascading_actions,
|
| 4284 |
-
"message": f"Delivery failed for order {order_id}. Reason: {reason_display}. Driver {driver_name} location updated to ({current_lat}, {current_lng})."
|
| 4285 |
}
|
| 4286 |
|
| 4287 |
except Exception as e:
|
|
|
|
| 1332 |
Execute order creation tool
|
| 1333 |
|
| 1334 |
Args:
|
| 1335 |
+
tool_input: Dict with order fields (expected_delivery_time now REQUIRED)
|
| 1336 |
|
| 1337 |
Returns:
|
| 1338 |
Order creation result
|
|
|
|
| 1344 |
delivery_address = tool_input.get("delivery_address")
|
| 1345 |
delivery_lat = tool_input.get("delivery_lat")
|
| 1346 |
delivery_lng = tool_input.get("delivery_lng")
|
| 1347 |
+
expected_delivery_time_str = tool_input.get("expected_delivery_time")
|
| 1348 |
priority = tool_input.get("priority", "standard")
|
| 1349 |
special_instructions = tool_input.get("special_instructions")
|
| 1350 |
weight_kg = tool_input.get("weight_kg", 5.0)
|
| 1351 |
+
sla_grace_period_minutes = tool_input.get("sla_grace_period_minutes", 15)
|
| 1352 |
|
| 1353 |
+
# Validate required fields (expected_delivery_time is now MANDATORY)
|
| 1354 |
+
if not all([customer_name, delivery_address, delivery_lat, delivery_lng, expected_delivery_time_str]):
|
| 1355 |
return {
|
| 1356 |
"success": False,
|
| 1357 |
+
"error": "Missing required fields: customer_name, delivery_address, delivery_lat, delivery_lng, expected_delivery_time"
|
| 1358 |
}
|
| 1359 |
|
| 1360 |
# Generate order ID with microseconds to prevent collisions
|
| 1361 |
now = datetime.now()
|
| 1362 |
order_id = f"ORD-{now.strftime('%Y%m%d%H%M%S%f')[:18]}" # YYYYMMDDHHMMSSΞΌΞΌΞΌΞΌΞΌΞΌ (18 chars)
|
| 1363 |
|
| 1364 |
+
# Parse and validate expected_delivery_time
|
| 1365 |
+
try:
|
| 1366 |
+
expected_delivery_time = datetime.fromisoformat(expected_delivery_time_str.replace('Z', '+00:00'))
|
| 1367 |
+
|
| 1368 |
+
# Validate it's in the future
|
| 1369 |
+
if expected_delivery_time <= now:
|
| 1370 |
+
return {
|
| 1371 |
+
"success": False,
|
| 1372 |
+
"error": f"expected_delivery_time must be in the future. Provided: {expected_delivery_time_str}, Current time: {now.isoformat()}"
|
| 1373 |
+
}
|
| 1374 |
+
except (ValueError, AttributeError) as e:
|
| 1375 |
+
return {
|
| 1376 |
+
"success": False,
|
| 1377 |
+
"error": f"Invalid expected_delivery_time format. Must be ISO 8601 format (e.g., '2025-11-15T18:00:00'). Error: {str(e)}"
|
| 1378 |
+
}
|
| 1379 |
+
|
| 1380 |
+
# Handle time window (kept for backward compatibility)
|
| 1381 |
time_window_end_str = tool_input.get("time_window_end")
|
| 1382 |
if time_window_end_str:
|
| 1383 |
try:
|
| 1384 |
time_window_end = datetime.fromisoformat(time_window_end_str.replace('Z', '+00:00'))
|
| 1385 |
except:
|
| 1386 |
+
time_window_end = expected_delivery_time # Use expected time as fallback
|
| 1387 |
else:
|
| 1388 |
+
time_window_end = expected_delivery_time # Default to expected delivery time
|
| 1389 |
|
| 1390 |
time_window_start = now + timedelta(hours=2)
|
| 1391 |
|
|
|
|
| 1394 |
INSERT INTO orders (
|
| 1395 |
order_id, customer_name, customer_phone, customer_email,
|
| 1396 |
delivery_address, delivery_lat, delivery_lng,
|
| 1397 |
+
time_window_start, time_window_end, expected_delivery_time,
|
| 1398 |
+
priority, weight_kg, status, special_instructions, sla_grace_period_minutes
|
| 1399 |
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
| 1400 |
"""
|
| 1401 |
|
| 1402 |
params = (
|
|
|
|
| 1409 |
delivery_lng,
|
| 1410 |
time_window_start,
|
| 1411 |
time_window_end,
|
| 1412 |
+
expected_delivery_time,
|
| 1413 |
priority,
|
| 1414 |
weight_kg,
|
| 1415 |
"pending",
|
| 1416 |
+
special_instructions,
|
| 1417 |
+
sla_grace_period_minutes
|
| 1418 |
)
|
| 1419 |
|
| 1420 |
try:
|
| 1421 |
execute_write(query, params)
|
| 1422 |
+
logger.info(f"Order created: {order_id}, expected delivery: {expected_delivery_time.strftime('%Y-%m-%d %H:%M')}")
|
| 1423 |
|
| 1424 |
return {
|
| 1425 |
"success": True,
|
|
|
|
| 1427 |
"status": "pending",
|
| 1428 |
"customer": customer_name,
|
| 1429 |
"address": delivery_address,
|
| 1430 |
+
"expected_delivery": expected_delivery_time.strftime("%Y-%m-%d %H:%M"),
|
| 1431 |
+
"sla_grace_period_minutes": sla_grace_period_minutes,
|
| 1432 |
"priority": priority,
|
| 1433 |
+
"message": f"Order {order_id} created successfully! Expected delivery: {expected_delivery_time.strftime('%Y-%m-%d %H:%M')}"
|
| 1434 |
}
|
| 1435 |
except Exception as e:
|
| 1436 |
logger.error(f"Database error creating order: {e}")
|
|
|
|
| 1454 |
name = tool_input.get("name")
|
| 1455 |
phone = tool_input.get("phone")
|
| 1456 |
email = tool_input.get("email")
|
| 1457 |
+
vehicle_type = tool_input.get("vehicle_type") # No default - REQUIRED
|
| 1458 |
vehicle_plate = tool_input.get("vehicle_plate")
|
| 1459 |
capacity_kg = tool_input.get("capacity_kg", 1000.0)
|
| 1460 |
capacity_m3 = tool_input.get("capacity_m3", 12.0)
|
| 1461 |
+
current_lat = tool_input.get("current_lat") # No default - REQUIRED
|
| 1462 |
+
current_lng = tool_input.get("current_lng") # No default - REQUIRED
|
| 1463 |
|
| 1464 |
# Convert skills to regular list (handles protobuf RepeatedComposite)
|
| 1465 |
skills_raw = tool_input.get("skills", [])
|
|
|
|
| 1467 |
|
| 1468 |
status = tool_input.get("status", "active")
|
| 1469 |
|
| 1470 |
+
# Validate ALL required fields (name, vehicle_type, current_lat, current_lng)
|
| 1471 |
+
if not all([name, vehicle_type, current_lat is not None, current_lng is not None]):
|
| 1472 |
+
return {
|
| 1473 |
+
"success": False,
|
| 1474 |
+
"error": "Missing required fields: name, vehicle_type, current_lat, current_lng. All fields are mandatory."
|
| 1475 |
+
}
|
| 1476 |
+
|
| 1477 |
+
# Validate coordinates are valid numbers
|
| 1478 |
+
try:
|
| 1479 |
+
current_lat = float(current_lat)
|
| 1480 |
+
current_lng = float(current_lng)
|
| 1481 |
+
except (ValueError, TypeError):
|
| 1482 |
+
return {
|
| 1483 |
+
"success": False,
|
| 1484 |
+
"error": "current_lat and current_lng must be valid numbers"
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
# Validate coordinates are within valid ranges
|
| 1488 |
+
if not (-90 <= current_lat <= 90):
|
| 1489 |
+
return {
|
| 1490 |
+
"success": False,
|
| 1491 |
+
"error": f"Invalid latitude {current_lat}. Must be between -90 and 90"
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
if not (-180 <= current_lng <= 180):
|
| 1495 |
return {
|
| 1496 |
"success": False,
|
| 1497 |
+
"error": f"Invalid longitude {current_lng}. Must be between -180 and 180"
|
| 1498 |
}
|
| 1499 |
|
| 1500 |
# Generate driver ID with microseconds to prevent collisions
|
| 1501 |
now = datetime.now()
|
| 1502 |
driver_id = f"DRV-{now.strftime('%Y%m%d%H%M%S%f')[:18]}" # YYYYMMDDHHMMSSΞΌΞΌΞΌΞΌΞΌΞΌ (18 chars)
|
| 1503 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1504 |
# Insert into database
|
| 1505 |
query = """
|
| 1506 |
INSERT INTO drivers (
|
|
|
|
| 2520 |
order_id, customer_name, customer_phone, customer_email,
|
| 2521 |
pickup_address, pickup_lat, pickup_lng,
|
| 2522 |
delivery_address, delivery_lat, delivery_lng,
|
| 2523 |
+
time_window_start, time_window_end, expected_delivery_time,
|
| 2524 |
priority, weight_kg, volume_m3, special_instructions,
|
| 2525 |
+
status, assigned_driver_id, delivery_status,
|
| 2526 |
+
created_at, updated_at, delivered_at, sla_grace_period_minutes,
|
| 2527 |
order_value, payment_status,
|
| 2528 |
requires_signature, is_fragile, requires_cold_storage
|
| 2529 |
FROM orders
|
|
|
|
| 2569 |
"volume_m3": float(row['volume_m3']) if row['volume_m3'] else None,
|
| 2570 |
"special_instructions": row['special_instructions']
|
| 2571 |
},
|
| 2572 |
+
"delivery_status": row['delivery_status'],
|
| 2573 |
+
"timing": {
|
| 2574 |
+
"expected_delivery_time": str(row['expected_delivery_time']) if row['expected_delivery_time'] else None,
|
| 2575 |
+
"delivered_at": str(row['delivered_at']) if row['delivered_at'] else None,
|
| 2576 |
+
"sla_grace_period_minutes": row['sla_grace_period_minutes']
|
| 2577 |
+
},
|
| 2578 |
"flags": {
|
| 2579 |
"requires_signature": row['requires_signature'],
|
| 2580 |
"is_fragile": row['is_fragile'],
|
|
|
|
| 2587 |
"assigned_driver_id": row['assigned_driver_id'],
|
| 2588 |
"timestamps": {
|
| 2589 |
"created_at": str(row['created_at']),
|
| 2590 |
+
"updated_at": str(row['updated_at']) if row['updated_at'] else None
|
|
|
|
| 2591 |
}
|
| 2592 |
}
|
| 2593 |
|
|
|
|
| 3965 |
conn = get_db_connection()
|
| 3966 |
cursor = conn.cursor()
|
| 3967 |
|
| 3968 |
+
# Get assignment and order details including timing fields
|
| 3969 |
cursor.execute("""
|
| 3970 |
SELECT
|
| 3971 |
a.status, a.order_id, a.driver_id,
|
| 3972 |
a.delivery_location_lat, a.delivery_location_lng, a.delivery_address,
|
| 3973 |
+
o.customer_name, o.expected_delivery_time, o.sla_grace_period_minutes,
|
| 3974 |
d.name as driver_name
|
| 3975 |
FROM assignments a
|
| 3976 |
JOIN orders o ON a.order_id = o.order_id
|
|
|
|
| 3995 |
delivery_address = assignment_row['delivery_address']
|
| 3996 |
customer_name = assignment_row['customer_name']
|
| 3997 |
driver_name = assignment_row['driver_name']
|
| 3998 |
+
expected_delivery_time = assignment_row['expected_delivery_time']
|
| 3999 |
+
sla_grace_period_minutes = assignment_row['sla_grace_period_minutes'] or 15
|
| 4000 |
|
| 4001 |
# Validate status
|
| 4002 |
if status not in ["active", "in_progress"]:
|
|
|
|
| 4051 |
|
| 4052 |
logger.info(f"Driver {driver_id} location updated to delivery address: ({delivery_lat}, {delivery_lng})")
|
| 4053 |
|
| 4054 |
+
# Step 3: Calculate delivery performance status
|
| 4055 |
+
delivery_status = "on_time" # Default
|
| 4056 |
+
timing_info = {
|
| 4057 |
+
"expected_delivery_time": expected_delivery_time.isoformat() if expected_delivery_time else None,
|
| 4058 |
+
"actual_delivery_time": completion_time.isoformat(),
|
| 4059 |
+
"sla_grace_period_minutes": sla_grace_period_minutes
|
| 4060 |
+
}
|
| 4061 |
+
|
| 4062 |
+
if expected_delivery_time:
|
| 4063 |
+
# Calculate grace period deadline
|
| 4064 |
+
from datetime import timedelta
|
| 4065 |
+
grace_deadline = expected_delivery_time + timedelta(minutes=sla_grace_period_minutes)
|
| 4066 |
+
|
| 4067 |
+
if completion_time <= expected_delivery_time:
|
| 4068 |
+
delivery_status = "on_time"
|
| 4069 |
+
timing_info["status"] = "On-time delivery"
|
| 4070 |
+
timing_info["delay_minutes"] = 0
|
| 4071 |
+
elif completion_time <= grace_deadline:
|
| 4072 |
+
delivery_status = "late"
|
| 4073 |
+
delay_minutes = int((completion_time - expected_delivery_time).total_seconds() / 60)
|
| 4074 |
+
timing_info["status"] = f"Late (within grace period)"
|
| 4075 |
+
timing_info["delay_minutes"] = delay_minutes
|
| 4076 |
+
else:
|
| 4077 |
+
delivery_status = "very_late"
|
| 4078 |
+
delay_minutes = int((completion_time - expected_delivery_time).total_seconds() / 60)
|
| 4079 |
+
timing_info["status"] = f"Very late (SLA violation)"
|
| 4080 |
+
timing_info["delay_minutes"] = delay_minutes
|
| 4081 |
+
|
| 4082 |
+
# Step 4: Update order status to delivered with timing info
|
| 4083 |
cursor.execute("""
|
| 4084 |
UPDATE orders
|
| 4085 |
+
SET status = 'delivered',
|
| 4086 |
+
delivered_at = %s,
|
| 4087 |
+
delivery_status = %s,
|
| 4088 |
+
updated_at = %s
|
| 4089 |
WHERE order_id = %s
|
| 4090 |
+
""", (completion_time, delivery_status, completion_time, order_id))
|
| 4091 |
+
|
| 4092 |
+
logger.info(f"Order {order_id} marked as delivered with status '{delivery_status}'")
|
| 4093 |
|
| 4094 |
# Step 4: Check if driver has other active assignments
|
| 4095 |
cursor.execute("""
|
|
|
|
| 4126 |
"customer_name": customer_name,
|
| 4127 |
"driver_name": driver_name,
|
| 4128 |
"completed_at": completion_time.isoformat(),
|
| 4129 |
+
"delivery_status": delivery_status,
|
| 4130 |
+
"timing": timing_info,
|
| 4131 |
"delivery_location": {
|
| 4132 |
"lat": float(delivery_lat),
|
| 4133 |
"lng": float(delivery_lng),
|
|
|
|
| 4138 |
"location_updated_at": completion_time.isoformat()
|
| 4139 |
},
|
| 4140 |
"cascading_actions": cascading_actions,
|
| 4141 |
+
"message": f"Delivery completed! Order {order_id} delivered by {driver_name}. Status: {timing_info.get('status', delivery_status)}. Driver location updated to delivery address."
|
| 4142 |
}
|
| 4143 |
|
| 4144 |
except Exception as e:
|
|
|
|
| 4237 |
conn = get_db_connection()
|
| 4238 |
cursor = conn.cursor()
|
| 4239 |
|
| 4240 |
+
# Get assignment and order details including timing fields
|
| 4241 |
cursor.execute("""
|
| 4242 |
SELECT
|
| 4243 |
a.status, a.order_id, a.driver_id,
|
| 4244 |
a.delivery_address,
|
| 4245 |
+
o.customer_name, o.expected_delivery_time, o.sla_grace_period_minutes,
|
| 4246 |
d.name as driver_name
|
| 4247 |
FROM assignments a
|
| 4248 |
JOIN orders o ON a.order_id = o.order_id
|
|
|
|
| 4265 |
delivery_address = assignment_row['delivery_address']
|
| 4266 |
customer_name = assignment_row['customer_name']
|
| 4267 |
driver_name = assignment_row['driver_name']
|
| 4268 |
+
expected_delivery_time = assignment_row['expected_delivery_time']
|
| 4269 |
+
sla_grace_period_minutes = assignment_row['sla_grace_period_minutes'] or 15
|
| 4270 |
|
| 4271 |
# Validate status
|
| 4272 |
if status not in ["active", "in_progress"]:
|
|
|
|
| 4313 |
|
| 4314 |
logger.info(f"Driver {driver_id} location updated to reported position: ({current_lat}, {current_lng})")
|
| 4315 |
|
| 4316 |
+
# Step 3: Calculate delivery performance status for failure
|
| 4317 |
+
delivery_status = "failed_on_time" # Default - failed but before deadline
|
| 4318 |
+
timing_info = {
|
| 4319 |
+
"expected_delivery_time": expected_delivery_time.isoformat() if expected_delivery_time else None,
|
| 4320 |
+
"failure_time": failure_time.isoformat(),
|
| 4321 |
+
"sla_grace_period_minutes": sla_grace_period_minutes
|
| 4322 |
+
}
|
| 4323 |
+
|
| 4324 |
+
if expected_delivery_time:
|
| 4325 |
+
if failure_time <= expected_delivery_time:
|
| 4326 |
+
delivery_status = "failed_on_time"
|
| 4327 |
+
timing_info["status"] = "Failed before deadline (attempted delivery on time)"
|
| 4328 |
+
else:
|
| 4329 |
+
delivery_status = "failed_late"
|
| 4330 |
+
delay_minutes = int((failure_time - expected_delivery_time).total_seconds() / 60)
|
| 4331 |
+
timing_info["status"] = f"Failed after deadline (late attempt)"
|
| 4332 |
+
timing_info["delay_minutes"] = delay_minutes
|
| 4333 |
+
|
| 4334 |
+
# Step 4: Update order status to failed with timing info
|
| 4335 |
cursor.execute("""
|
| 4336 |
UPDATE orders
|
| 4337 |
+
SET status = 'failed',
|
| 4338 |
+
delivered_at = %s,
|
| 4339 |
+
delivery_status = %s,
|
| 4340 |
+
updated_at = %s
|
| 4341 |
WHERE order_id = %s
|
| 4342 |
+
""", (failure_time, delivery_status, failure_time, order_id))
|
| 4343 |
+
|
| 4344 |
+
logger.info(f"Order {order_id} marked as failed with status '{delivery_status}'")
|
| 4345 |
|
| 4346 |
# Step 4: Check if driver has other active assignments
|
| 4347 |
cursor.execute("""
|
|
|
|
| 4383 |
"failed_at": failure_time.isoformat(),
|
| 4384 |
"failure_reason": failure_reason,
|
| 4385 |
"failure_reason_display": reason_display,
|
| 4386 |
+
"delivery_status": delivery_status,
|
| 4387 |
+
"timing": timing_info,
|
| 4388 |
"delivery_address": delivery_address,
|
| 4389 |
"driver_location": {
|
| 4390 |
"lat": current_lat,
|
|
|
|
| 4392 |
"updated_at": failure_time.isoformat()
|
| 4393 |
},
|
| 4394 |
"cascading_actions": cascading_actions,
|
| 4395 |
+
"message": f"Delivery failed for order {order_id}. Reason: {reason_display}. Timing: {timing_info.get('status', delivery_status)}. Driver {driver_name} location updated to ({current_lat}, {current_lng})."
|
| 4396 |
}
|
| 4397 |
|
| 4398 |
except Exception as e:
|
database/migrations/006_add_delivery_timing.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Migration 006: Add delivery timing and SLA tracking fields to orders table
|
| 3 |
+
Adds expected_delivery_time (mandatory), delivery_status, and sla_grace_period_minutes
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Add parent directory to path for imports
|
| 10 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 11 |
+
|
| 12 |
+
from database.connection import get_db_connection
|
| 13 |
+
|
| 14 |
+
MIGRATION_SQL = """
|
| 15 |
+
-- Add expected delivery time (mandatory deadline promised to customer)
|
| 16 |
+
ALTER TABLE orders
|
| 17 |
+
ADD COLUMN IF NOT EXISTS expected_delivery_time TIMESTAMP;
|
| 18 |
+
|
| 19 |
+
-- Add delivery performance status
|
| 20 |
+
ALTER TABLE orders
|
| 21 |
+
ADD COLUMN IF NOT EXISTS delivery_status VARCHAR(20)
|
| 22 |
+
CHECK(delivery_status IN ('on_time', 'late', 'very_late', 'failed_on_time', 'failed_late'));
|
| 23 |
+
|
| 24 |
+
-- Add SLA grace period (minutes after expected time that's still acceptable)
|
| 25 |
+
ALTER TABLE orders
|
| 26 |
+
ADD COLUMN IF NOT EXISTS sla_grace_period_minutes INTEGER DEFAULT 15;
|
| 27 |
+
|
| 28 |
+
-- Add comments
|
| 29 |
+
COMMENT ON COLUMN orders.expected_delivery_time IS 'Required delivery deadline promised to customer (mandatory when creating order)';
|
| 30 |
+
COMMENT ON COLUMN orders.delivery_status IS 'Delivery performance: on_time, late (within grace), very_late (SLA violation), failed_on_time, failed_late';
|
| 31 |
+
COMMENT ON COLUMN orders.sla_grace_period_minutes IS 'Grace period in minutes after expected_delivery_time (default: 15 mins)';
|
| 32 |
+
|
| 33 |
+
-- Create index for querying by expected delivery time
|
| 34 |
+
CREATE INDEX IF NOT EXISTS idx_orders_expected_delivery ON orders(expected_delivery_time);
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
ROLLBACK_SQL = """
|
| 38 |
+
-- Drop indexes
|
| 39 |
+
DROP INDEX IF EXISTS idx_orders_expected_delivery;
|
| 40 |
+
|
| 41 |
+
-- Drop columns
|
| 42 |
+
ALTER TABLE orders
|
| 43 |
+
DROP COLUMN IF EXISTS expected_delivery_time,
|
| 44 |
+
DROP COLUMN IF EXISTS delivery_status,
|
| 45 |
+
DROP COLUMN IF EXISTS sla_grace_period_minutes;
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def up():
|
| 50 |
+
"""Apply migration - add delivery timing fields"""
|
| 51 |
+
print("Running migration 006: Add delivery timing and SLA tracking fields...")
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
conn = get_db_connection()
|
| 55 |
+
cursor = conn.cursor()
|
| 56 |
+
|
| 57 |
+
# Execute migration SQL
|
| 58 |
+
cursor.execute(MIGRATION_SQL)
|
| 59 |
+
|
| 60 |
+
conn.commit()
|
| 61 |
+
cursor.close()
|
| 62 |
+
conn.close()
|
| 63 |
+
|
| 64 |
+
print("SUCCESS: Migration 006 applied successfully")
|
| 65 |
+
print(" - Added expected_delivery_time TIMESTAMP column")
|
| 66 |
+
print(" - Added delivery_status VARCHAR(20) column")
|
| 67 |
+
print(" - Added sla_grace_period_minutes INTEGER column (default: 15)")
|
| 68 |
+
print(" - Created index on expected_delivery_time")
|
| 69 |
+
print(" - Valid delivery statuses: on_time, late, very_late, failed_on_time, failed_late")
|
| 70 |
+
return True
|
| 71 |
+
|
| 72 |
+
except Exception as e:
|
| 73 |
+
print(f"ERROR: Migration 006 failed: {e}")
|
| 74 |
+
return False
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def down():
|
| 78 |
+
"""Rollback migration - drop delivery timing fields"""
|
| 79 |
+
print("Rolling back migration 006: Drop delivery timing fields...")
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
conn = get_db_connection()
|
| 83 |
+
cursor = conn.cursor()
|
| 84 |
+
|
| 85 |
+
# Execute rollback SQL
|
| 86 |
+
cursor.execute(ROLLBACK_SQL)
|
| 87 |
+
|
| 88 |
+
conn.commit()
|
| 89 |
+
cursor.close()
|
| 90 |
+
conn.close()
|
| 91 |
+
|
| 92 |
+
print("SUCCESS: Migration 006 rolled back successfully")
|
| 93 |
+
return True
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"ERROR: Migration 006 rollback failed: {e}")
|
| 97 |
+
return False
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
import sys
|
| 102 |
+
|
| 103 |
+
if len(sys.argv) > 1 and sys.argv[1] == "down":
|
| 104 |
+
down()
|
| 105 |
+
else:
|
| 106 |
+
up()
|
server.py
CHANGED
|
@@ -299,27 +299,38 @@ def create_order(
|
|
| 299 |
delivery_address: str,
|
| 300 |
delivery_lat: float,
|
| 301 |
delivery_lng: float,
|
|
|
|
| 302 |
customer_phone: str | None = None,
|
| 303 |
customer_email: str | None = None,
|
| 304 |
priority: Literal["standard", "express", "urgent"] = "standard",
|
| 305 |
weight_kg: float = 5.0,
|
| 306 |
special_instructions: str | None = None,
|
|
|
|
| 307 |
time_window_end: str | None = None
|
| 308 |
) -> dict:
|
| 309 |
"""
|
| 310 |
-
Create a new delivery order in the database
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
Args:
|
| 313 |
customer_name: Full name of the customer
|
| 314 |
delivery_address: Complete delivery address
|
| 315 |
delivery_lat: Latitude from geocoding
|
| 316 |
delivery_lng: Longitude from geocoding
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
customer_phone: Customer phone number (optional)
|
| 318 |
customer_email: Customer email address (optional)
|
| 319 |
priority: Delivery priority level (default: standard)
|
| 320 |
weight_kg: Package weight in kilograms (default: 5.0)
|
| 321 |
special_instructions: Special delivery instructions (optional)
|
| 322 |
-
|
|
|
|
|
|
|
| 323 |
|
| 324 |
Returns:
|
| 325 |
dict: {
|
|
@@ -328,23 +339,26 @@ def create_order(
|
|
| 328 |
status: str,
|
| 329 |
customer: str,
|
| 330 |
address: str,
|
| 331 |
-
|
|
|
|
| 332 |
priority: str,
|
| 333 |
message: str
|
| 334 |
}
|
| 335 |
"""
|
| 336 |
from chat.tools import handle_create_order
|
| 337 |
-
logger.info(f"Tool: create_order(customer='{customer_name}',
|
| 338 |
return handle_create_order({
|
| 339 |
"customer_name": customer_name,
|
| 340 |
"delivery_address": delivery_address,
|
| 341 |
"delivery_lat": delivery_lat,
|
| 342 |
"delivery_lng": delivery_lng,
|
|
|
|
| 343 |
"customer_phone": customer_phone,
|
| 344 |
"customer_email": customer_email,
|
| 345 |
"priority": priority,
|
| 346 |
"weight_kg": weight_kg,
|
| 347 |
"special_instructions": special_instructions,
|
|
|
|
| 348 |
"time_window_end": time_window_end
|
| 349 |
})
|
| 350 |
|
|
|
|
| 299 |
delivery_address: str,
|
| 300 |
delivery_lat: float,
|
| 301 |
delivery_lng: float,
|
| 302 |
+
expected_delivery_time: str,
|
| 303 |
customer_phone: str | None = None,
|
| 304 |
customer_email: str | None = None,
|
| 305 |
priority: Literal["standard", "express", "urgent"] = "standard",
|
| 306 |
weight_kg: float = 5.0,
|
| 307 |
special_instructions: str | None = None,
|
| 308 |
+
sla_grace_period_minutes: int = 15,
|
| 309 |
time_window_end: str | None = None
|
| 310 |
) -> dict:
|
| 311 |
"""
|
| 312 |
+
Create a new delivery order in the database with MANDATORY delivery deadline.
|
| 313 |
+
|
| 314 |
+
IMPORTANT: expected_delivery_time is REQUIRED. This is the promised delivery time to the customer.
|
| 315 |
+
Only call this after geocoding the address successfully.
|
| 316 |
|
| 317 |
Args:
|
| 318 |
customer_name: Full name of the customer
|
| 319 |
delivery_address: Complete delivery address
|
| 320 |
delivery_lat: Latitude from geocoding
|
| 321 |
delivery_lng: Longitude from geocoding
|
| 322 |
+
expected_delivery_time: REQUIRED - Promised delivery deadline in ISO 8601 format
|
| 323 |
+
Must be future timestamp. Examples:
|
| 324 |
+
- '2025-11-15T18:00:00' (6 PM today)
|
| 325 |
+
- '2025-11-16T12:00:00' (noon tomorrow)
|
| 326 |
customer_phone: Customer phone number (optional)
|
| 327 |
customer_email: Customer email address (optional)
|
| 328 |
priority: Delivery priority level (default: standard)
|
| 329 |
weight_kg: Package weight in kilograms (default: 5.0)
|
| 330 |
special_instructions: Special delivery instructions (optional)
|
| 331 |
+
sla_grace_period_minutes: Grace period after deadline (default: 15 mins)
|
| 332 |
+
Deliveries within grace period marked as 'late' but acceptable
|
| 333 |
+
time_window_end: Legacy field, defaults to expected_delivery_time if not provided
|
| 334 |
|
| 335 |
Returns:
|
| 336 |
dict: {
|
|
|
|
| 339 |
status: str,
|
| 340 |
customer: str,
|
| 341 |
address: str,
|
| 342 |
+
expected_delivery: str (new),
|
| 343 |
+
sla_grace_period_minutes: int (new),
|
| 344 |
priority: str,
|
| 345 |
message: str
|
| 346 |
}
|
| 347 |
"""
|
| 348 |
from chat.tools import handle_create_order
|
| 349 |
+
logger.info(f"Tool: create_order(customer='{customer_name}', expected_delivery='{expected_delivery_time}')")
|
| 350 |
return handle_create_order({
|
| 351 |
"customer_name": customer_name,
|
| 352 |
"delivery_address": delivery_address,
|
| 353 |
"delivery_lat": delivery_lat,
|
| 354 |
"delivery_lng": delivery_lng,
|
| 355 |
+
"expected_delivery_time": expected_delivery_time,
|
| 356 |
"customer_phone": customer_phone,
|
| 357 |
"customer_email": customer_email,
|
| 358 |
"priority": priority,
|
| 359 |
"weight_kg": weight_kg,
|
| 360 |
"special_instructions": special_instructions,
|
| 361 |
+
"sla_grace_period_minutes": sla_grace_period_minutes,
|
| 362 |
"time_window_end": time_window_end
|
| 363 |
})
|
| 364 |
|
test_delivery_timing.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script for delivery timing and SLA tracking
|
| 3 |
+
Verifies:
|
| 4 |
+
- expected_delivery_time is mandatory when creating orders
|
| 5 |
+
- On-time delivery detection
|
| 6 |
+
- Late delivery detection
|
| 7 |
+
- Very late delivery detection (SLA violation)
|
| 8 |
+
- Failed delivery timing tracking
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import sys
|
| 12 |
+
import os
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
+
|
| 16 |
+
from chat.tools import (
|
| 17 |
+
handle_create_order,
|
| 18 |
+
handle_create_driver,
|
| 19 |
+
handle_create_assignment,
|
| 20 |
+
handle_complete_delivery,
|
| 21 |
+
handle_fail_delivery,
|
| 22 |
+
handle_get_order_details
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
print("=" * 70)
|
| 26 |
+
print("Testing Delivery Timing & SLA Tracking")
|
| 27 |
+
print("=" * 70)
|
| 28 |
+
|
| 29 |
+
# Test 1: Create order WITHOUT expected_delivery_time (should fail)
|
| 30 |
+
print("\n[1] Testing: Create order WITHOUT expected_delivery_time (should fail)...")
|
| 31 |
+
result = handle_create_order({
|
| 32 |
+
"customer_name": "Test Customer",
|
| 33 |
+
"delivery_address": "Test Address",
|
| 34 |
+
"delivery_lat": 23.7808,
|
| 35 |
+
"delivery_lng": 90.4130,
|
| 36 |
+
"priority": "standard"
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
if not result.get("success"):
|
| 40 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 41 |
+
if "expected_delivery_time" in result.get('error', '').lower():
|
| 42 |
+
print("SUCCESS: Correctly requires expected_delivery_time!")
|
| 43 |
+
else:
|
| 44 |
+
print("WARNING: Error message should mention expected_delivery_time")
|
| 45 |
+
else:
|
| 46 |
+
print("FAILED: Should have required expected_delivery_time!")
|
| 47 |
+
sys.exit(1)
|
| 48 |
+
|
| 49 |
+
# Test 2: Create order with PAST expected_delivery_time (should fail)
|
| 50 |
+
print("\n[2] Testing: Create order with PAST delivery time (should fail)...")
|
| 51 |
+
past_time = (datetime.now() - timedelta(hours=2)).isoformat()
|
| 52 |
+
result = handle_create_order({
|
| 53 |
+
"customer_name": "Test Customer",
|
| 54 |
+
"delivery_address": "Test Address",
|
| 55 |
+
"delivery_lat": 23.7808,
|
| 56 |
+
"delivery_lng": 90.4130,
|
| 57 |
+
"expected_delivery_time": past_time,
|
| 58 |
+
"priority": "standard"
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
if not result.get("success"):
|
| 62 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 63 |
+
if "future" in result.get('error', '').lower():
|
| 64 |
+
print("SUCCESS: Correctly validates expected_delivery_time must be in future!")
|
| 65 |
+
else:
|
| 66 |
+
print("WARNING: Error should mention time must be in future")
|
| 67 |
+
else:
|
| 68 |
+
print("FAILED: Should have rejected past delivery time!")
|
| 69 |
+
sys.exit(1)
|
| 70 |
+
|
| 71 |
+
# Test 3: Create valid order with expected_delivery_time
|
| 72 |
+
print("\n[3] Creating valid order with expected_delivery_time...")
|
| 73 |
+
# Set deadline to 2 hours from now
|
| 74 |
+
expected_time = datetime.now() + timedelta(hours=2)
|
| 75 |
+
order_result = handle_create_order({
|
| 76 |
+
"customer_name": "Timing Test Customer",
|
| 77 |
+
"customer_phone": "+8801712345678",
|
| 78 |
+
"delivery_address": "Bashundhara, Dhaka",
|
| 79 |
+
"delivery_lat": 23.8223,
|
| 80 |
+
"delivery_lng": 90.4259,
|
| 81 |
+
"expected_delivery_time": expected_time.isoformat(),
|
| 82 |
+
"priority": "standard",
|
| 83 |
+
"weight_kg": 2.5,
|
| 84 |
+
"sla_grace_period_minutes": 15
|
| 85 |
+
})
|
| 86 |
+
|
| 87 |
+
if not order_result.get("success"):
|
| 88 |
+
print(f"FAILED: {order_result.get('error')}")
|
| 89 |
+
sys.exit(1)
|
| 90 |
+
|
| 91 |
+
order_id = order_result["order_id"]
|
| 92 |
+
print(f"SUCCESS: Order created: {order_id}")
|
| 93 |
+
print(f" Expected delivery: {order_result.get('expected_delivery')}")
|
| 94 |
+
print(f" SLA grace period: {order_result.get('sla_grace_period_minutes')} minutes")
|
| 95 |
+
|
| 96 |
+
# Test 4: Create driver
|
| 97 |
+
print("\n[4] Creating test driver...")
|
| 98 |
+
import time
|
| 99 |
+
time.sleep(0.1) # Avoid ID collision
|
| 100 |
+
driver_result = handle_create_driver({
|
| 101 |
+
"name": "Timing Test Driver",
|
| 102 |
+
"phone": "+8801812345678",
|
| 103 |
+
"vehicle_type": "motorcycle",
|
| 104 |
+
"current_lat": 23.7549,
|
| 105 |
+
"current_lng": 90.3909
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
if not driver_result.get("success"):
|
| 109 |
+
print(f"FAILED: {driver_result.get('error')}")
|
| 110 |
+
sys.exit(1)
|
| 111 |
+
|
| 112 |
+
driver_id = driver_result["driver_id"]
|
| 113 |
+
print(f"SUCCESS: Driver created: {driver_id}")
|
| 114 |
+
|
| 115 |
+
# Test 5: Create assignment
|
| 116 |
+
print("\n[5] Creating assignment...")
|
| 117 |
+
assignment_result = handle_create_assignment({
|
| 118 |
+
"order_id": order_id,
|
| 119 |
+
"driver_id": driver_id
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
if not assignment_result.get("success"):
|
| 123 |
+
print(f"FAILED: {assignment_result.get('error')}")
|
| 124 |
+
sys.exit(1)
|
| 125 |
+
|
| 126 |
+
assignment_id = assignment_result["assignment_id"]
|
| 127 |
+
print(f"SUCCESS: Assignment created: {assignment_id}")
|
| 128 |
+
|
| 129 |
+
# Test 6: Simulate ON-TIME delivery (complete before expected_delivery_time)
|
| 130 |
+
print("\n[6] Testing ON-TIME delivery (before deadline)...")
|
| 131 |
+
# Since we can't control system time, we check the logic exists
|
| 132 |
+
completion_result = handle_complete_delivery({
|
| 133 |
+
"assignment_id": assignment_id,
|
| 134 |
+
"confirm": True,
|
| 135 |
+
"notes": "Delivered on time"
|
| 136 |
+
})
|
| 137 |
+
|
| 138 |
+
if not completion_result.get("success"):
|
| 139 |
+
print(f"FAILED: {completion_result.get('error')}")
|
| 140 |
+
sys.exit(1)
|
| 141 |
+
|
| 142 |
+
print(f"SUCCESS: Delivery completed!")
|
| 143 |
+
print(f" Delivery status: {completion_result.get('delivery_status')}")
|
| 144 |
+
print(f" Timing info: {completion_result.get('timing', {})}")
|
| 145 |
+
|
| 146 |
+
# Check timing fields
|
| 147 |
+
timing = completion_result.get('timing', {})
|
| 148 |
+
if 'expected_delivery_time' in timing and 'actual_delivery_time' in timing:
|
| 149 |
+
print("SUCCESS: Timing information tracked correctly!")
|
| 150 |
+
print(f" Expected: {timing.get('expected_delivery_time')}")
|
| 151 |
+
print(f" Actual: {timing.get('actual_delivery_time')}")
|
| 152 |
+
print(f" Status: {timing.get('status')}")
|
| 153 |
+
if 'delay_minutes' in timing:
|
| 154 |
+
print(f" Delay: {timing.get('delay_minutes')} minutes")
|
| 155 |
+
else:
|
| 156 |
+
print("FAILED: Missing timing information!")
|
| 157 |
+
|
| 158 |
+
# Test 7: Verify delivered_at was set (BUG FIX)
|
| 159 |
+
print("\n[7] Verifying delivered_at field was set (BUG FIX test)...")
|
| 160 |
+
order_details = handle_get_order_details({"order_id": order_id})
|
| 161 |
+
if order_details.get("success"):
|
| 162 |
+
order = order_details.get("order", {})
|
| 163 |
+
timing_data = order.get("timing", {})
|
| 164 |
+
delivered_at = timing_data.get("delivered_at")
|
| 165 |
+
delivery_status = order.get("delivery_status")
|
| 166 |
+
|
| 167 |
+
if delivered_at:
|
| 168 |
+
print(f"SUCCESS: delivered_at field was set: {delivered_at}")
|
| 169 |
+
else:
|
| 170 |
+
print("FAILED: delivered_at field was NOT set (BUG NOT FIXED!)")
|
| 171 |
+
|
| 172 |
+
if delivery_status:
|
| 173 |
+
print(f"SUCCESS: delivery_status field was set: {delivery_status}")
|
| 174 |
+
else:
|
| 175 |
+
print("WARNING: delivery_status field was NOT set")
|
| 176 |
+
else:
|
| 177 |
+
print(f"FAILED to get order details: {order_details.get('error')}")
|
| 178 |
+
|
| 179 |
+
# Cleanup
|
| 180 |
+
print("\n" + "=" * 70)
|
| 181 |
+
print("Cleaning up test data...")
|
| 182 |
+
from chat.tools import handle_delete_order, handle_delete_driver
|
| 183 |
+
|
| 184 |
+
handle_delete_order({"order_id": order_id, "confirm": True})
|
| 185 |
+
handle_delete_driver({"driver_id": driver_id, "confirm": True})
|
| 186 |
+
print("Cleanup complete!")
|
| 187 |
+
|
| 188 |
+
# Test 8: Test failed delivery timing
|
| 189 |
+
print("\n" + "=" * 70)
|
| 190 |
+
print("[8] Testing FAILED delivery with timing tracking...")
|
| 191 |
+
|
| 192 |
+
# Create new order
|
| 193 |
+
time.sleep(0.1)
|
| 194 |
+
expected_time2 = datetime.now() + timedelta(hours=1)
|
| 195 |
+
order_result2 = handle_create_order({
|
| 196 |
+
"customer_name": "Failed Timing Test",
|
| 197 |
+
"customer_phone": "+8801712345679",
|
| 198 |
+
"delivery_address": "Mirpur, Dhaka",
|
| 199 |
+
"delivery_lat": 23.8103,
|
| 200 |
+
"delivery_lng": 90.4125,
|
| 201 |
+
"expected_delivery_time": expected_time2.isoformat(),
|
| 202 |
+
"priority": "standard"
|
| 203 |
+
})
|
| 204 |
+
|
| 205 |
+
if not order_result2.get("success"):
|
| 206 |
+
print(f"FAILED: {order_result2.get('error')}")
|
| 207 |
+
sys.exit(1)
|
| 208 |
+
|
| 209 |
+
order_id2 = order_result2["order_id"]
|
| 210 |
+
print(f"Order created: {order_id2}")
|
| 211 |
+
|
| 212 |
+
# Create driver
|
| 213 |
+
time.sleep(0.1)
|
| 214 |
+
driver_result2 = handle_create_driver({
|
| 215 |
+
"name": "Failed Test Driver",
|
| 216 |
+
"phone": "+8801812345679",
|
| 217 |
+
"vehicle_type": "car",
|
| 218 |
+
"current_lat": 23.7465,
|
| 219 |
+
"current_lng": 90.3760
|
| 220 |
+
})
|
| 221 |
+
|
| 222 |
+
driver_id2 = driver_result2["driver_id"]
|
| 223 |
+
print(f"Driver created: {driver_id2}")
|
| 224 |
+
|
| 225 |
+
# Create assignment
|
| 226 |
+
assignment_result2 = handle_create_assignment({
|
| 227 |
+
"order_id": order_id2,
|
| 228 |
+
"driver_id": driver_id2
|
| 229 |
+
})
|
| 230 |
+
|
| 231 |
+
assignment_id2 = assignment_result2["assignment_id"]
|
| 232 |
+
print(f"Assignment created: {assignment_id2}")
|
| 233 |
+
|
| 234 |
+
# Fail delivery
|
| 235 |
+
failure_result = handle_fail_delivery({
|
| 236 |
+
"assignment_id": assignment_id2,
|
| 237 |
+
"current_lat": 23.7600,
|
| 238 |
+
"current_lng": 90.3850,
|
| 239 |
+
"failure_reason": "customer_not_available",
|
| 240 |
+
"confirm": True,
|
| 241 |
+
"notes": "Customer phone was off"
|
| 242 |
+
})
|
| 243 |
+
|
| 244 |
+
if failure_result.get("success"):
|
| 245 |
+
print(f"SUCCESS: Failure recorded!")
|
| 246 |
+
print(f" Delivery status: {failure_result.get('delivery_status')}")
|
| 247 |
+
print(f" Timing info: {failure_result.get('timing', {})}")
|
| 248 |
+
|
| 249 |
+
timing = failure_result.get('timing', {})
|
| 250 |
+
if 'expected_delivery_time' in timing and 'failure_time' in timing:
|
| 251 |
+
print("SUCCESS: Failure timing information tracked!")
|
| 252 |
+
print(f" Expected: {timing.get('expected_delivery_time')}")
|
| 253 |
+
print(f" Failed at: {timing.get('failure_time')}")
|
| 254 |
+
print(f" Status: {timing.get('status')}")
|
| 255 |
+
else:
|
| 256 |
+
print(f"FAILED: {failure_result.get('error')}")
|
| 257 |
+
|
| 258 |
+
# Cleanup
|
| 259 |
+
print("\nCleaning up failed delivery test data...")
|
| 260 |
+
handle_delete_order({"order_id": order_id2, "confirm": True})
|
| 261 |
+
handle_delete_driver({"driver_id": driver_id2, "confirm": True})
|
| 262 |
+
print("Cleanup complete!")
|
| 263 |
+
|
| 264 |
+
print("\n" + "=" * 70)
|
| 265 |
+
print("Delivery Timing & SLA Tracking Test Complete!")
|
| 266 |
+
print("=" * 70)
|
| 267 |
+
print("\nSummary:")
|
| 268 |
+
print(" - expected_delivery_time is mandatory: YES")
|
| 269 |
+
print(" - Past delivery times rejected: YES")
|
| 270 |
+
print(" - delivered_at field gets set: YES (BUG FIXED)")
|
| 271 |
+
print(" - delivery_status tracked: YES")
|
| 272 |
+
print(" - Timing information returned: YES")
|
| 273 |
+
print(" - Failed delivery timing tracked: YES")
|
test_driver_validation.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script for driver creation validation
|
| 3 |
+
Verifies that drivers cannot be created without required fields:
|
| 4 |
+
- name
|
| 5 |
+
- vehicle_type
|
| 6 |
+
- current_lat
|
| 7 |
+
- current_lng
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
import os
|
| 12 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 13 |
+
|
| 14 |
+
from chat.tools import handle_create_driver
|
| 15 |
+
|
| 16 |
+
print("=" * 70)
|
| 17 |
+
print("Testing Driver Creation Validation")
|
| 18 |
+
print("=" * 70)
|
| 19 |
+
|
| 20 |
+
# Test 1: Create driver WITHOUT name (should fail)
|
| 21 |
+
print("\n[1] Testing: Create driver WITHOUT name (should fail)...")
|
| 22 |
+
result = handle_create_driver({
|
| 23 |
+
"vehicle_type": "motorcycle",
|
| 24 |
+
"current_lat": 23.8103,
|
| 25 |
+
"current_lng": 90.4125
|
| 26 |
+
})
|
| 27 |
+
|
| 28 |
+
if not result.get("success"):
|
| 29 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 30 |
+
if "name" in result.get('error', '').lower():
|
| 31 |
+
print("SUCCESS: Correctly requires name!")
|
| 32 |
+
else:
|
| 33 |
+
print("WARNING: Error message should mention 'name'")
|
| 34 |
+
else:
|
| 35 |
+
print(f"FAILED: Should have required name! Created: {result.get('driver_id')}")
|
| 36 |
+
sys.exit(1)
|
| 37 |
+
|
| 38 |
+
# Test 2: Create driver WITHOUT vehicle_type (should fail)
|
| 39 |
+
print("\n[2] Testing: Create driver WITHOUT vehicle_type (should fail)...")
|
| 40 |
+
result = handle_create_driver({
|
| 41 |
+
"name": "Test Driver",
|
| 42 |
+
"current_lat": 23.8103,
|
| 43 |
+
"current_lng": 90.4125
|
| 44 |
+
})
|
| 45 |
+
|
| 46 |
+
if not result.get("success"):
|
| 47 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 48 |
+
if "vehicle_type" in result.get('error', '').lower():
|
| 49 |
+
print("SUCCESS: Correctly requires vehicle_type!")
|
| 50 |
+
else:
|
| 51 |
+
print("WARNING: Error message should mention 'vehicle_type'")
|
| 52 |
+
else:
|
| 53 |
+
print(f"FAILED: Should have required vehicle_type! Created: {result.get('driver_id')}")
|
| 54 |
+
sys.exit(1)
|
| 55 |
+
|
| 56 |
+
# Test 3: Create driver WITHOUT current_lat (should fail)
|
| 57 |
+
print("\n[3] Testing: Create driver WITHOUT current_lat (should fail)...")
|
| 58 |
+
result = handle_create_driver({
|
| 59 |
+
"name": "Test Driver",
|
| 60 |
+
"vehicle_type": "motorcycle",
|
| 61 |
+
"current_lng": 90.4125
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
if not result.get("success"):
|
| 65 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 66 |
+
if "current_lat" in result.get('error', '').lower():
|
| 67 |
+
print("SUCCESS: Correctly requires current_lat!")
|
| 68 |
+
else:
|
| 69 |
+
print("WARNING: Error message should mention 'current_lat'")
|
| 70 |
+
else:
|
| 71 |
+
print(f"FAILED: Should have required current_lat! Created: {result.get('driver_id')}")
|
| 72 |
+
sys.exit(1)
|
| 73 |
+
|
| 74 |
+
# Test 4: Create driver WITHOUT current_lng (should fail)
|
| 75 |
+
print("\n[4] Testing: Create driver WITHOUT current_lng (should fail)...")
|
| 76 |
+
result = handle_create_driver({
|
| 77 |
+
"name": "Test Driver",
|
| 78 |
+
"vehicle_type": "motorcycle",
|
| 79 |
+
"current_lat": 23.8103
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
if not result.get("success"):
|
| 83 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 84 |
+
if "current_lng" in result.get('error', '').lower():
|
| 85 |
+
print("SUCCESS: Correctly requires current_lng!")
|
| 86 |
+
else:
|
| 87 |
+
print("WARNING: Error message should mention 'current_lng'")
|
| 88 |
+
else:
|
| 89 |
+
print(f"FAILED: Should have required current_lng! Created: {result.get('driver_id')}")
|
| 90 |
+
sys.exit(1)
|
| 91 |
+
|
| 92 |
+
# Test 5: Create driver with INVALID latitude (should fail)
|
| 93 |
+
print("\n[5] Testing: Create driver with invalid latitude (should fail)...")
|
| 94 |
+
result = handle_create_driver({
|
| 95 |
+
"name": "Test Driver",
|
| 96 |
+
"vehicle_type": "motorcycle",
|
| 97 |
+
"current_lat": 95.0, # Invalid - must be -90 to 90
|
| 98 |
+
"current_lng": 90.4125
|
| 99 |
+
})
|
| 100 |
+
|
| 101 |
+
if not result.get("success"):
|
| 102 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 103 |
+
if "latitude" in result.get('error', '').lower() or "-90" in result.get('error', ''):
|
| 104 |
+
print("SUCCESS: Correctly validates latitude range!")
|
| 105 |
+
else:
|
| 106 |
+
print("WARNING: Error message should mention latitude validation")
|
| 107 |
+
else:
|
| 108 |
+
print(f"FAILED: Should have rejected invalid latitude!")
|
| 109 |
+
sys.exit(1)
|
| 110 |
+
|
| 111 |
+
# Test 6: Create driver with INVALID longitude (should fail)
|
| 112 |
+
print("\n[6] Testing: Create driver with invalid longitude (should fail)...")
|
| 113 |
+
result = handle_create_driver({
|
| 114 |
+
"name": "Test Driver",
|
| 115 |
+
"vehicle_type": "motorcycle",
|
| 116 |
+
"current_lat": 23.8103,
|
| 117 |
+
"current_lng": 200.0 # Invalid - must be -180 to 180
|
| 118 |
+
})
|
| 119 |
+
|
| 120 |
+
if not result.get("success"):
|
| 121 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 122 |
+
if "longitude" in result.get('error', '').lower() or "-180" in result.get('error', ''):
|
| 123 |
+
print("SUCCESS: Correctly validates longitude range!")
|
| 124 |
+
else:
|
| 125 |
+
print("WARNING: Error message should mention longitude validation")
|
| 126 |
+
else:
|
| 127 |
+
print(f"FAILED: Should have rejected invalid longitude!")
|
| 128 |
+
sys.exit(1)
|
| 129 |
+
|
| 130 |
+
# Test 7: Create driver with NON-NUMERIC coordinates (should fail)
|
| 131 |
+
print("\n[7] Testing: Create driver with non-numeric coordinates (should fail)...")
|
| 132 |
+
result = handle_create_driver({
|
| 133 |
+
"name": "Test Driver",
|
| 134 |
+
"vehicle_type": "motorcycle",
|
| 135 |
+
"current_lat": "not a number",
|
| 136 |
+
"current_lng": 90.4125
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
if not result.get("success"):
|
| 140 |
+
print(f"EXPECTED FAILURE: {result.get('error')}")
|
| 141 |
+
if "number" in result.get('error', '').lower() or "valid" in result.get('error', '').lower():
|
| 142 |
+
print("SUCCESS: Correctly validates coordinates are numbers!")
|
| 143 |
+
else:
|
| 144 |
+
print("WARNING: Error message should mention coordinates must be numbers")
|
| 145 |
+
else:
|
| 146 |
+
print(f"FAILED: Should have rejected non-numeric coordinates!")
|
| 147 |
+
sys.exit(1)
|
| 148 |
+
|
| 149 |
+
# Test 8: Create driver WITH all required fields (should succeed)
|
| 150 |
+
print("\n[8] Testing: Create driver with ALL required fields (should succeed)...")
|
| 151 |
+
result = handle_create_driver({
|
| 152 |
+
"name": "Valid Test Driver",
|
| 153 |
+
"phone": "+8801812345678",
|
| 154 |
+
"vehicle_type": "motorcycle",
|
| 155 |
+
"current_lat": 23.8103,
|
| 156 |
+
"current_lng": 90.4125
|
| 157 |
+
})
|
| 158 |
+
|
| 159 |
+
if result.get("success"):
|
| 160 |
+
driver_id = result.get("driver_id")
|
| 161 |
+
print(f"SUCCESS: Driver created: {driver_id}")
|
| 162 |
+
print(f" Name: {result.get('name')}")
|
| 163 |
+
print(f" Vehicle: {result.get('vehicle_type')}")
|
| 164 |
+
print(f" Location: ({result.get('location', {}).get('latitude')}, {result.get('location', {}).get('longitude')})")
|
| 165 |
+
|
| 166 |
+
# Cleanup
|
| 167 |
+
print("\nCleaning up test driver...")
|
| 168 |
+
from chat.tools import handle_delete_driver
|
| 169 |
+
handle_delete_driver({"driver_id": driver_id, "confirm": True})
|
| 170 |
+
print("Cleanup complete!")
|
| 171 |
+
else:
|
| 172 |
+
print(f"FAILED: Should have created driver with all required fields!")
|
| 173 |
+
print(f"Error: {result.get('error')}")
|
| 174 |
+
sys.exit(1)
|
| 175 |
+
|
| 176 |
+
print("\n" + "=" * 70)
|
| 177 |
+
print("Driver Creation Validation Test Complete!")
|
| 178 |
+
print("=" * 70)
|
| 179 |
+
print("\nSummary:")
|
| 180 |
+
print(" - name is mandatory: YES")
|
| 181 |
+
print(" - vehicle_type is mandatory: YES")
|
| 182 |
+
print(" - current_lat is mandatory: YES")
|
| 183 |
+
print(" - current_lng is mandatory: YES")
|
| 184 |
+
print(" - Latitude range validated (-90 to 90): YES")
|
| 185 |
+
print(" - Longitude range validated (-180 to 180): YES")
|
| 186 |
+
print(" - Coordinates must be numeric: YES")
|
| 187 |
+
print(" - Valid driver can be created: YES")
|