mashrur950 commited on
Commit
5b558e5
·
1 Parent(s): 796f6b6

Implement migrations for assignments management and testing framework

Browse files

- Migration 002: Create assignments table with route tracking and metadata
- Migration 003: Add foreign key constraint to orders.assigned_driver_id
- Migration 004: Add route_directions column to assignments table for navigation instructions
- Migration 005: Add failure_reason column to assignments table for structured failure tracking
- Add comprehensive test suite for assignment system including creation, updates, and failure scenarios
- Implement tests for delivery completion, route directions storage, duplicate assignment prevention, and failure handling

MCP_TOOLS_SUMMARY.md CHANGED
@@ -14,8 +14,8 @@
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
18
- 11. **`delete_order`** - Delete order (requires confirmation)
19
 
20
  ## Driver Management (8 tools)
21
 
@@ -25,22 +25,55 @@
25
  15. **`get_driver_details`** - Get full details of specific driver by ID
26
  16. **`search_drivers`** - Search drivers by name, phone, or driver ID
27
  17. **`get_available_drivers`** - Get all active drivers ready for assignment
28
- 18. **`update_driver`** - Update driver status, phone, vehicle type, location
29
- 19. **`delete_driver`** - Delete driver (requires confirmation)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  ---
32
 
33
- ## Total: 19 MCP Tools
34
 
35
  **Routing Tools:** 3 (with Google Routes API integration)
36
- **Order Tools:** 8 (full CRUD + search)
37
- **Driver Tools:** 8 (full CRUD + search)
 
 
38
 
39
  ### Key Features:
40
  - ✅ Real-time traffic & weather-aware routing
41
  - ✅ Vehicle-specific optimization (motorcycle/bicycle/car/van/truck)
42
  - ✅ Toll detection & avoidance
43
- - ✅ Complete fleet management (orders + drivers)
44
- - ✅ PostgreSQL database backend
 
 
 
 
 
 
45
  - ✅ Search & filtering capabilities
46
- - ✅ Status tracking & updates
 
 
 
 
 
 
 
 
 
 
 
 
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)
18
+ 11. **`delete_order`** - Delete order (with active assignment checks)
19
 
20
  ## Driver Management (8 tools)
21
 
 
25
  15. **`get_driver_details`** - Get full details of specific driver by ID
26
  16. **`search_drivers`** - Search drivers by name, phone, or driver ID
27
  17. **`get_available_drivers`** - Get all active drivers ready for assignment
28
+ 18. **`update_driver`** - Update driver status, phone, vehicle type, location (with assignment validation)
29
+ 19. **`delete_driver`** - Delete driver (with assignment safety checks)
30
+
31
+ ## Assignment Management (6 tools)
32
+
33
+ 20. **`create_assignment`** - Assign order to driver (validates status, calculates route, saves all data)
34
+ 21. **`get_assignment_details`** - Get assignment details by assignment ID, order ID, or driver ID
35
+ 22. **`update_assignment`** - Update assignment status with cascading updates to orders/drivers
36
+ 23. **`unassign_order`** - Unassign order from driver (reverts statuses, requires confirmation)
37
+ 24. **`complete_delivery`** - Mark delivery complete and auto-update driver location to delivery address
38
+ 25. **`fail_delivery`** - Mark delivery as failed with MANDATORY driver location and failure reason
39
+
40
+ ## Bulk Operations (2 tools)
41
+
42
+ 26. **`delete_all_orders`** - Bulk delete all orders (or by status filter, blocks if active assignments exist)
43
+ 27. **`delete_all_drivers`** - Bulk delete all drivers (or by status filter, blocks if assignments exist)
44
 
45
  ---
46
 
47
+ ## Total: 27 MCP Tools
48
 
49
  **Routing Tools:** 3 (with Google Routes API integration)
50
+ **Order Tools:** 8 (full CRUD + search + cascading)
51
+ **Driver Tools:** 8 (full CRUD + search + cascading)
52
+ **Assignment Tools:** 6 (complete assignment lifecycle + delivery completion + failure handling)
53
+ **Bulk Operations:** 2 (efficient mass deletions with safety checks)
54
 
55
  ### Key Features:
56
  - ✅ Real-time traffic & weather-aware routing
57
  - ✅ Vehicle-specific optimization (motorcycle/bicycle/car/van/truck)
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**
64
+ - ✅ Cascading status updates (order → assignment → driver)
65
+ - ✅ Safety checks preventing invalid deletions/updates
66
+ - ✅ PostgreSQL database with foreign key constraints
67
  - ✅ Search & filtering capabilities
68
+ - ✅ Status tracking & validation
69
+
70
+ ### Assignment System Capabilities:
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
78
+ - **Assignment lifecycle**: active → in_progress → completed/failed/cancelled
79
+ - **Database integrity** via FK constraints (ON DELETE CASCADE/RESTRICT/SET NULL)
chat/tools.py CHANGED
@@ -11,7 +11,7 @@ import logging
11
  # Add parent directory to path
12
  sys.path.insert(0, str(Path(__file__).parent.parent))
13
 
14
- from database.connection import execute_write, execute_query
15
  from chat.geocoding import GeocodingService
16
 
17
  logger = logging.getLogger(__name__)
@@ -1355,9 +1355,9 @@ def handle_create_order(tool_input: dict) -> dict:
1355
  "error": "Missing required fields: customer_name, delivery_address, delivery_lat, delivery_lng"
1356
  }
1357
 
1358
- # Generate order ID
1359
  now = datetime.now()
1360
- order_id = f"ORD-{now.strftime('%Y%m%d%H%M%S')}"
1361
 
1362
  # Handle time window
1363
  time_window_end_str = tool_input.get("time_window_end")
@@ -1451,9 +1451,9 @@ def handle_create_driver(tool_input: dict) -> dict:
1451
  "error": "Missing required field: name"
1452
  }
1453
 
1454
- # Generate driver ID
1455
  now = datetime.now()
1456
- driver_id = f"DRV-{now.strftime('%Y%m%d%H%M%S')}"
1457
 
1458
  # Default location (San Francisco)
1459
  current_lat = tool_input.get("current_lat", 37.7749)
@@ -1514,7 +1514,7 @@ def handle_create_driver(tool_input: dict) -> dict:
1514
 
1515
  def handle_update_order(tool_input: dict) -> dict:
1516
  """
1517
- Execute order update tool
1518
 
1519
  Args:
1520
  tool_input: Dict with order_id and fields to update
@@ -1533,8 +1533,8 @@ def handle_update_order(tool_input: dict) -> dict:
1533
  "error": "Missing required field: order_id"
1534
  }
1535
 
1536
- # Check if order exists
1537
- check_query = "SELECT order_id FROM orders WHERE order_id = %s"
1538
  existing = execute_query(check_query, (order_id,))
1539
 
1540
  if not existing:
@@ -1543,6 +1543,9 @@ def handle_update_order(tool_input: dict) -> dict:
1543
  "error": f"Order {order_id} not found"
1544
  }
1545
 
 
 
 
1546
  # Auto-geocode if delivery address is updated without coordinates
1547
  if "delivery_address" in tool_input and ("delivery_lat" not in tool_input or "delivery_lng" not in tool_input):
1548
  from chat.geocoding import GeocodingService
@@ -1556,6 +1559,103 @@ def handle_update_order(tool_input: dict) -> dict:
1556
  except Exception as e:
1557
  logger.warning(f"Failed to geocode address, skipping coordinate update: {e}")
1558
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1559
  # Build UPDATE query dynamically based on provided fields
1560
  update_fields = []
1561
  params = []
@@ -1606,12 +1706,17 @@ def handle_update_order(tool_input: dict) -> dict:
1606
  execute_write(query, tuple(params))
1607
  logger.info(f"Order updated: {order_id}")
1608
 
1609
- return {
1610
  "success": True,
1611
  "order_id": order_id,
1612
  "updated_fields": list(updateable_fields.keys() & tool_input.keys()),
1613
  "message": f"Order {order_id} updated successfully!"
1614
  }
 
 
 
 
 
1615
  except Exception as e:
1616
  logger.error(f"Database error updating order: {e}")
1617
  return {
@@ -1620,9 +1725,82 @@ def handle_update_order(tool_input: dict) -> dict:
1620
  }
1621
 
1622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1623
  def handle_delete_order(tool_input: dict) -> dict:
1624
  """
1625
- Execute order deletion tool
1626
 
1627
  Args:
1628
  tool_input: Dict with order_id and confirm flag
@@ -1656,18 +1834,53 @@ def handle_delete_order(tool_input: dict) -> dict:
1656
  "error": f"Order {order_id} not found"
1657
  }
1658
 
1659
- # Delete the order
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1660
  query = "DELETE FROM orders WHERE order_id = %s"
1661
 
1662
  try:
1663
  execute_write(query, (order_id,))
1664
  logger.info(f"Order deleted: {order_id}")
1665
 
1666
- return {
1667
  "success": True,
1668
  "order_id": order_id,
1669
  "message": f"Order {order_id} has been permanently deleted."
1670
  }
 
 
 
 
 
1671
  except Exception as e:
1672
  logger.error(f"Database error deleting order: {e}")
1673
  return {
@@ -1678,7 +1891,7 @@ def handle_delete_order(tool_input: dict) -> dict:
1678
 
1679
  def handle_update_driver(tool_input: dict) -> dict:
1680
  """
1681
- Execute driver update tool
1682
 
1683
  Args:
1684
  tool_input: Dict with driver_id and fields to update
@@ -1697,8 +1910,8 @@ def handle_update_driver(tool_input: dict) -> dict:
1697
  "error": "Missing required field: driver_id"
1698
  }
1699
 
1700
- # Check if driver exists
1701
- check_query = "SELECT driver_id FROM drivers WHERE driver_id = %s"
1702
  existing = execute_query(check_query, (driver_id,))
1703
 
1704
  if not existing:
@@ -1707,6 +1920,35 @@ def handle_update_driver(tool_input: dict) -> dict:
1707
  "error": f"Driver {driver_id} not found"
1708
  }
1709
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1710
  # Build UPDATE query dynamically based on provided fields
1711
  update_fields = []
1712
  params = []
@@ -1783,9 +2025,87 @@ def handle_update_driver(tool_input: dict) -> dict:
1783
  }
1784
 
1785
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1786
  def handle_delete_driver(tool_input: dict) -> dict:
1787
  """
1788
- Execute driver deletion tool
1789
 
1790
  Args:
1791
  tool_input: Dict with driver_id and confirm flag
@@ -1821,6 +2141,53 @@ def handle_delete_driver(tool_input: dict) -> dict:
1821
 
1822
  driver_name = existing[0]["name"]
1823
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1824
  # Delete the driver
1825
  query = "DELETE FROM drivers WHERE driver_id = %s"
1826
 
@@ -1828,16 +2195,27 @@ def handle_delete_driver(tool_input: dict) -> dict:
1828
  execute_write(query, (driver_id,))
1829
  logger.info(f"Driver deleted: {driver_id}")
1830
 
1831
- return {
1832
  "success": True,
1833
  "driver_id": driver_id,
1834
  "message": f"Driver {driver_id} ({driver_name}) has been permanently deleted."
1835
  }
 
 
 
 
 
1836
  except Exception as e:
1837
  logger.error(f"Database error deleting driver: {e}")
 
 
 
 
 
 
1838
  return {
1839
  "success": False,
1840
- "error": f"Failed to delete driver: {str(e)}"
1841
  }
1842
 
1843
 
@@ -2777,6 +3155,1143 @@ def handle_get_available_drivers(tool_input: dict) -> dict:
2777
  }
2778
 
2779
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2780
  def get_tools_list() -> list:
2781
  """Get list of available tools"""
2782
  return [tool["name"] for tool in TOOLS_SCHEMA]
 
11
  # Add parent directory to path
12
  sys.path.insert(0, str(Path(__file__).parent.parent))
13
 
14
+ from database.connection import execute_write, execute_query, get_db_connection
15
  from chat.geocoding import GeocodingService
16
 
17
  logger = logging.getLogger(__name__)
 
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
  # Handle time window
1363
  time_window_end_str = tool_input.get("time_window_end")
 
1451
  "error": "Missing required field: name"
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)
 
1514
 
1515
  def handle_update_order(tool_input: dict) -> dict:
1516
  """
1517
+ Execute order update tool with assignment cascading logic
1518
 
1519
  Args:
1520
  tool_input: Dict with order_id and fields to update
 
1533
  "error": "Missing required field: order_id"
1534
  }
1535
 
1536
+ # Check if order exists and get current status
1537
+ check_query = "SELECT order_id, status, assigned_driver_id FROM orders WHERE order_id = %s"
1538
  existing = execute_query(check_query, (order_id,))
1539
 
1540
  if not existing:
 
1543
  "error": f"Order {order_id} not found"
1544
  }
1545
 
1546
+ current_status = existing[0].get("status")
1547
+ current_assigned_driver = existing[0].get("assigned_driver_id")
1548
+
1549
  # Auto-geocode if delivery address is updated without coordinates
1550
  if "delivery_address" in tool_input and ("delivery_lat" not in tool_input or "delivery_lng" not in tool_input):
1551
  from chat.geocoding import GeocodingService
 
1559
  except Exception as e:
1560
  logger.warning(f"Failed to geocode address, skipping coordinate update: {e}")
1561
 
1562
+ # Handle status changes with assignment cascading logic
1563
+ new_status = tool_input.get("status")
1564
+ cascading_actions = []
1565
+
1566
+ if new_status and new_status != current_status:
1567
+ # Check if order has active assignment
1568
+ assignment_check = execute_query("""
1569
+ SELECT assignment_id, status, driver_id
1570
+ FROM assignments
1571
+ WHERE order_id = %s AND status IN ('active', 'in_progress')
1572
+ LIMIT 1
1573
+ """, (order_id,))
1574
+
1575
+ has_active_assignment = len(assignment_check) > 0
1576
+
1577
+ # Validate status transitions based on assignment state
1578
+ if new_status == "pending" and current_status == "assigned":
1579
+ if has_active_assignment:
1580
+ # Changing assigned order back to pending - must cancel assignment
1581
+ assignment_id = assignment_check[0]["assignment_id"]
1582
+ driver_id = assignment_check[0]["driver_id"]
1583
+
1584
+ # Cancel the assignment
1585
+ execute_write("""
1586
+ UPDATE assignments SET status = 'cancelled', updated_at = %s
1587
+ WHERE assignment_id = %s
1588
+ """, (datetime.now(), assignment_id))
1589
+
1590
+ # Clear assigned_driver_id from order
1591
+ execute_write("""
1592
+ UPDATE orders SET assigned_driver_id = NULL
1593
+ WHERE order_id = %s
1594
+ """, (order_id,))
1595
+
1596
+ # Check if driver has other active assignments
1597
+ other_assignments = execute_query("""
1598
+ SELECT COUNT(*) as count FROM assignments
1599
+ WHERE driver_id = %s AND status IN ('active', 'in_progress')
1600
+ AND assignment_id != %s
1601
+ """, (driver_id, assignment_id))
1602
+
1603
+ if other_assignments[0]["count"] == 0:
1604
+ # Set driver back to active if no other assignments
1605
+ execute_write("""
1606
+ UPDATE drivers SET status = 'active', updated_at = %s
1607
+ WHERE driver_id = %s
1608
+ """, (datetime.now(), driver_id))
1609
+ cascading_actions.append(f"Driver {driver_id} set to active (no other assignments)")
1610
+
1611
+ cascading_actions.append(f"Assignment {assignment_id} cancelled and removed")
1612
+
1613
+ elif new_status == "cancelled":
1614
+ if has_active_assignment:
1615
+ # Cancel active assignment when order is cancelled
1616
+ assignment_id = assignment_check[0]["assignment_id"]
1617
+ driver_id = assignment_check[0]["driver_id"]
1618
+
1619
+ execute_write("""
1620
+ UPDATE assignments SET status = 'cancelled', updated_at = %s
1621
+ WHERE assignment_id = %s
1622
+ """, (datetime.now(), assignment_id))
1623
+
1624
+ # Clear assigned_driver_id
1625
+ execute_write("""
1626
+ UPDATE orders SET assigned_driver_id = NULL
1627
+ WHERE order_id = %s
1628
+ """, (order_id,))
1629
+
1630
+ # Check if driver has other active assignments
1631
+ other_assignments = execute_query("""
1632
+ SELECT COUNT(*) as count FROM assignments
1633
+ WHERE driver_id = %s AND status IN ('active', 'in_progress')
1634
+ AND assignment_id != %s
1635
+ """, (driver_id, assignment_id))
1636
+
1637
+ if other_assignments[0]["count"] == 0:
1638
+ execute_write("""
1639
+ UPDATE drivers SET status = 'active', updated_at = %s
1640
+ WHERE driver_id = %s
1641
+ """, (datetime.now(), driver_id))
1642
+ cascading_actions.append(f"Driver {driver_id} set to active")
1643
+
1644
+ cascading_actions.append(f"Assignment {assignment_id} cancelled")
1645
+
1646
+ elif new_status in ["delivered", "failed"] and has_active_assignment:
1647
+ # Note: This should normally be handled by update_assignment tool
1648
+ # but we allow it here for flexibility
1649
+ assignment_id = assignment_check[0]["assignment_id"]
1650
+ final_status = "completed" if new_status == "delivered" else "failed"
1651
+
1652
+ execute_write("""
1653
+ UPDATE assignments SET status = %s, updated_at = %s
1654
+ WHERE assignment_id = %s
1655
+ """, (final_status, datetime.now(), assignment_id))
1656
+
1657
+ cascading_actions.append(f"Assignment {assignment_id} marked as {final_status}")
1658
+
1659
  # Build UPDATE query dynamically based on provided fields
1660
  update_fields = []
1661
  params = []
 
1706
  execute_write(query, tuple(params))
1707
  logger.info(f"Order updated: {order_id}")
1708
 
1709
+ result = {
1710
  "success": True,
1711
  "order_id": order_id,
1712
  "updated_fields": list(updateable_fields.keys() & tool_input.keys()),
1713
  "message": f"Order {order_id} updated successfully!"
1714
  }
1715
+
1716
+ if cascading_actions:
1717
+ result["cascading_actions"] = cascading_actions
1718
+
1719
+ return result
1720
  except Exception as e:
1721
  logger.error(f"Database error updating order: {e}")
1722
  return {
 
1725
  }
1726
 
1727
 
1728
+ def handle_delete_all_orders(tool_input: dict) -> dict:
1729
+ """
1730
+ Delete all orders (bulk delete)
1731
+
1732
+ Args:
1733
+ tool_input: Dict with confirm flag and optional status filter
1734
+
1735
+ Returns:
1736
+ Deletion result with count
1737
+ """
1738
+ confirm = tool_input.get("confirm", False)
1739
+ status_filter = tool_input.get("status") # Optional: delete only specific status
1740
+
1741
+ if not confirm:
1742
+ return {
1743
+ "success": False,
1744
+ "error": "Bulk deletion requires confirm=true for safety"
1745
+ }
1746
+
1747
+ try:
1748
+ # Check for active assignments first
1749
+ active_assignments = execute_query("""
1750
+ SELECT COUNT(*) as count FROM assignments
1751
+ WHERE status IN ('active', 'in_progress')
1752
+ """)
1753
+
1754
+ active_count = active_assignments[0]['count']
1755
+
1756
+ if active_count > 0:
1757
+ return {
1758
+ "success": False,
1759
+ "error": f"Cannot delete orders: {active_count} active assignment(s) exist. Cancel or complete them first."
1760
+ }
1761
+
1762
+ # Build delete query based on status filter
1763
+ if status_filter:
1764
+ count_query = "SELECT COUNT(*) as count FROM orders WHERE status = %s"
1765
+ delete_query = "DELETE FROM orders WHERE status = %s"
1766
+ params = (status_filter,)
1767
+ else:
1768
+ count_query = "SELECT COUNT(*) as count FROM orders"
1769
+ delete_query = "DELETE FROM orders"
1770
+ params = ()
1771
+
1772
+ # Get count before deletion
1773
+ count_result = execute_query(count_query, params)
1774
+ total_count = count_result[0]['count']
1775
+
1776
+ if total_count == 0:
1777
+ return {
1778
+ "success": True,
1779
+ "deleted_count": 0,
1780
+ "message": "No orders to delete"
1781
+ }
1782
+
1783
+ # Execute bulk delete
1784
+ execute_write(delete_query, params)
1785
+ logger.info(f"Bulk deleted {total_count} orders")
1786
+
1787
+ return {
1788
+ "success": True,
1789
+ "deleted_count": total_count,
1790
+ "message": f"Successfully deleted {total_count} order(s)"
1791
+ }
1792
+
1793
+ except Exception as e:
1794
+ logger.error(f"Database error bulk deleting orders: {e}")
1795
+ return {
1796
+ "success": False,
1797
+ "error": f"Failed to bulk delete orders: {str(e)}"
1798
+ }
1799
+
1800
+
1801
  def handle_delete_order(tool_input: dict) -> dict:
1802
  """
1803
+ Execute order deletion tool with assignment safety checks
1804
 
1805
  Args:
1806
  tool_input: Dict with order_id and confirm flag
 
1834
  "error": f"Order {order_id} not found"
1835
  }
1836
 
1837
+ order_status = existing[0].get("status")
1838
+
1839
+ # Check for active assignments
1840
+ assignment_check = execute_query("""
1841
+ SELECT assignment_id, status, driver_id
1842
+ FROM assignments
1843
+ WHERE order_id = %s AND status IN ('active', 'in_progress')
1844
+ """, (order_id,))
1845
+
1846
+ if assignment_check:
1847
+ # Warn about active assignments that will be cascade deleted
1848
+ assignment_count = len(assignment_check)
1849
+ assignment_ids = [a["assignment_id"] for a in assignment_check]
1850
+
1851
+ return {
1852
+ "success": False,
1853
+ "error": f"Cannot delete order {order_id}: it has {assignment_count} active assignment(s): {', '.join(assignment_ids)}. Please cancel or complete the assignment(s) first using update_assignment or unassign_order.",
1854
+ "active_assignments": assignment_ids
1855
+ }
1856
+
1857
+ # Check for any completed assignments (these will be cascade deleted)
1858
+ completed_assignments = execute_query("""
1859
+ SELECT COUNT(*) as count FROM assignments
1860
+ WHERE order_id = %s AND status IN ('completed', 'failed', 'cancelled')
1861
+ """, (order_id,))
1862
+
1863
+ cascading_info = []
1864
+ if completed_assignments[0]["count"] > 0:
1865
+ cascading_info.append(f"{completed_assignments[0]['count']} completed/failed/cancelled assignment(s) will be cascade deleted")
1866
+
1867
+ # Delete the order (will cascade to assignments via FK)
1868
  query = "DELETE FROM orders WHERE order_id = %s"
1869
 
1870
  try:
1871
  execute_write(query, (order_id,))
1872
  logger.info(f"Order deleted: {order_id}")
1873
 
1874
+ result = {
1875
  "success": True,
1876
  "order_id": order_id,
1877
  "message": f"Order {order_id} has been permanently deleted."
1878
  }
1879
+
1880
+ if cascading_info:
1881
+ result["cascading_info"] = cascading_info
1882
+
1883
+ return result
1884
  except Exception as e:
1885
  logger.error(f"Database error deleting order: {e}")
1886
  return {
 
1891
 
1892
  def handle_update_driver(tool_input: dict) -> dict:
1893
  """
1894
+ Execute driver update tool with assignment validation
1895
 
1896
  Args:
1897
  tool_input: Dict with driver_id and fields to update
 
1910
  "error": "Missing required field: driver_id"
1911
  }
1912
 
1913
+ # Check if driver exists and get current status
1914
+ check_query = "SELECT driver_id, status FROM drivers WHERE driver_id = %s"
1915
  existing = execute_query(check_query, (driver_id,))
1916
 
1917
  if not existing:
 
1920
  "error": f"Driver {driver_id} not found"
1921
  }
1922
 
1923
+ current_status = existing[0].get("status")
1924
+
1925
+ # Validate status changes against active assignments
1926
+ new_status = tool_input.get("status")
1927
+ if new_status and new_status != current_status:
1928
+ # Check for active assignments
1929
+ assignment_check = execute_query("""
1930
+ SELECT assignment_id, status, order_id
1931
+ FROM assignments
1932
+ WHERE driver_id = %s AND status IN ('active', 'in_progress')
1933
+ """, (driver_id,))
1934
+
1935
+ has_active_assignments = len(assignment_check) > 0
1936
+
1937
+ # Prevent setting driver to offline/inactive when they have active assignments
1938
+ if new_status in ["offline", "inactive"] and has_active_assignments:
1939
+ assignment_count = len(assignment_check)
1940
+ assignment_ids = [a["assignment_id"] for a in assignment_check]
1941
+
1942
+ return {
1943
+ "success": False,
1944
+ "error": f"Cannot set driver {driver_id} to '{new_status}': driver has {assignment_count} active assignment(s): {', '.join(assignment_ids)}. Please complete or cancel assignments first.",
1945
+ "active_assignments": assignment_ids
1946
+ }
1947
+
1948
+ # Note: Setting driver to 'active' when they have assignments is allowed
1949
+ # The system manages 'busy' status automatically via assignment creation
1950
+ # But we allow manual override to 'active' for edge cases
1951
+
1952
  # Build UPDATE query dynamically based on provided fields
1953
  update_fields = []
1954
  params = []
 
2025
  }
2026
 
2027
 
2028
+ def handle_delete_all_drivers(tool_input: dict) -> dict:
2029
+ """
2030
+ Delete all drivers (bulk delete)
2031
+
2032
+ Args:
2033
+ tool_input: Dict with confirm flag and optional status filter
2034
+
2035
+ Returns:
2036
+ Deletion result with count
2037
+ """
2038
+ confirm = tool_input.get("confirm", False)
2039
+ status_filter = tool_input.get("status") # Optional: delete only specific status
2040
+
2041
+ if not confirm:
2042
+ return {
2043
+ "success": False,
2044
+ "error": "Bulk deletion requires confirm=true for safety"
2045
+ }
2046
+
2047
+ try:
2048
+ # Check for ANY assignments (RESTRICT constraint will block if any exist)
2049
+ assignments = execute_query("""
2050
+ SELECT COUNT(*) as count FROM assignments
2051
+ """)
2052
+
2053
+ assignment_count = assignments[0]['count']
2054
+
2055
+ if assignment_count > 0:
2056
+ return {
2057
+ "success": False,
2058
+ "error": f"Cannot delete drivers: {assignment_count} assignment(s) exist in database. Database RESTRICT constraint prevents driver deletion when assignments exist."
2059
+ }
2060
+
2061
+ # Build delete query based on status filter
2062
+ if status_filter:
2063
+ count_query = "SELECT COUNT(*) as count FROM drivers WHERE status = %s"
2064
+ delete_query = "DELETE FROM drivers WHERE status = %s"
2065
+ params = (status_filter,)
2066
+ else:
2067
+ count_query = "SELECT COUNT(*) as count FROM drivers"
2068
+ delete_query = "DELETE FROM drivers"
2069
+ params = ()
2070
+
2071
+ # Get count before deletion
2072
+ count_result = execute_query(count_query, params)
2073
+ total_count = count_result[0]['count']
2074
+
2075
+ if total_count == 0:
2076
+ return {
2077
+ "success": True,
2078
+ "deleted_count": 0,
2079
+ "message": "No drivers to delete"
2080
+ }
2081
+
2082
+ # Execute bulk delete
2083
+ execute_write(delete_query, params)
2084
+ logger.info(f"Bulk deleted {total_count} drivers")
2085
+
2086
+ return {
2087
+ "success": True,
2088
+ "deleted_count": total_count,
2089
+ "message": f"Successfully deleted {total_count} driver(s)"
2090
+ }
2091
+
2092
+ except Exception as e:
2093
+ logger.error(f"Database error bulk deleting drivers: {e}")
2094
+
2095
+ # Provide more context if it's a FK constraint error
2096
+ error_message = str(e)
2097
+ if "foreign key" in error_message.lower() or "violates" in error_message.lower():
2098
+ error_message = f"Cannot delete drivers due to database constraint (assignments exist). Error: {error_message}"
2099
+
2100
+ return {
2101
+ "success": False,
2102
+ "error": f"Failed to bulk delete drivers: {error_message}"
2103
+ }
2104
+
2105
+
2106
  def handle_delete_driver(tool_input: dict) -> dict:
2107
  """
2108
+ Execute driver deletion tool with assignment safety checks
2109
 
2110
  Args:
2111
  tool_input: Dict with driver_id and confirm flag
 
2141
 
2142
  driver_name = existing[0]["name"]
2143
 
2144
+ # Check for ANY assignments (active or completed)
2145
+ # FK constraint with ON DELETE RESTRICT will prevent deletion if ANY assignments exist
2146
+ assignment_check = execute_query("""
2147
+ SELECT assignment_id, status, order_id
2148
+ FROM assignments
2149
+ WHERE driver_id = %s
2150
+ """, (driver_id,))
2151
+
2152
+ if assignment_check:
2153
+ # Count active vs completed assignments
2154
+ active_assignments = [a for a in assignment_check if a["status"] in ("active", "in_progress")]
2155
+ completed_assignments = [a for a in assignment_check if a["status"] in ("completed", "failed", "cancelled")]
2156
+
2157
+ total_count = len(assignment_check)
2158
+ active_count = len(active_assignments)
2159
+ completed_count = len(completed_assignments)
2160
+
2161
+ error_msg = f"Cannot delete driver {driver_id} ({driver_name}): driver has {total_count} assignment(s)"
2162
+
2163
+ if active_count > 0:
2164
+ active_ids = [a["assignment_id"] for a in active_assignments]
2165
+ error_msg += f" ({active_count} active: {', '.join(active_ids)})"
2166
+
2167
+ if completed_count > 0:
2168
+ error_msg += f" ({completed_count} completed/failed/cancelled)"
2169
+
2170
+ error_msg += ". The database has RESTRICT constraint preventing driver deletion when assignments exist. Please cancel/complete active assignments and consider archiving the driver instead of deleting."
2171
+
2172
+ return {
2173
+ "success": False,
2174
+ "error": error_msg,
2175
+ "total_assignments": total_count,
2176
+ "active_assignments": [a["assignment_id"] for a in active_assignments],
2177
+ "completed_assignments": [a["assignment_id"] for a in completed_assignments]
2178
+ }
2179
+
2180
+ # Check for orders that reference this driver in assigned_driver_id
2181
+ # FK constraint with ON DELETE SET NULL will set these to NULL
2182
+ assigned_orders = execute_query("""
2183
+ SELECT order_id FROM orders WHERE assigned_driver_id = %s
2184
+ """, (driver_id,))
2185
+
2186
+ cascading_info = []
2187
+ if assigned_orders:
2188
+ order_count = len(assigned_orders)
2189
+ cascading_info.append(f"{order_count} order(s) will have assigned_driver_id set to NULL")
2190
+
2191
  # Delete the driver
2192
  query = "DELETE FROM drivers WHERE driver_id = %s"
2193
 
 
2195
  execute_write(query, (driver_id,))
2196
  logger.info(f"Driver deleted: {driver_id}")
2197
 
2198
+ result = {
2199
  "success": True,
2200
  "driver_id": driver_id,
2201
  "message": f"Driver {driver_id} ({driver_name}) has been permanently deleted."
2202
  }
2203
+
2204
+ if cascading_info:
2205
+ result["cascading_info"] = cascading_info
2206
+
2207
+ return result
2208
  except Exception as e:
2209
  logger.error(f"Database error deleting driver: {e}")
2210
+
2211
+ # Provide more context if it's a FK constraint error
2212
+ error_message = str(e)
2213
+ if "foreign key" in error_message.lower() or "violates" in error_message.lower():
2214
+ error_message = f"Cannot delete driver due to database constraint (likely has related assignments). Error: {error_message}"
2215
+
2216
  return {
2217
  "success": False,
2218
+ "error": f"Failed to delete driver: {error_message}"
2219
  }
2220
 
2221
 
 
3155
  }
3156
 
3157
 
3158
+ # ============================================================================
3159
+ # ASSIGNMENT MANAGEMENT TOOLS
3160
+ # ============================================================================
3161
+
3162
+ def handle_create_assignment(tool_input: dict) -> dict:
3163
+ """
3164
+ Create assignment (assign order to driver)
3165
+
3166
+ Validates order and driver status, calculates route, creates assignment record,
3167
+ and updates order/driver statuses.
3168
+
3169
+ Args:
3170
+ tool_input: Dict with order_id and driver_id
3171
+
3172
+ Returns:
3173
+ Assignment creation result with route data
3174
+ """
3175
+ from datetime import datetime, timedelta
3176
+
3177
+ order_id = (tool_input.get("order_id") or "").strip()
3178
+ driver_id = (tool_input.get("driver_id") or "").strip()
3179
+
3180
+ if not order_id or not driver_id:
3181
+ return {
3182
+ "success": False,
3183
+ "error": "Both order_id and driver_id are required"
3184
+ }
3185
+
3186
+ logger.info(f"Creating assignment: order={order_id}, driver={driver_id}")
3187
+
3188
+ try:
3189
+ conn = get_db_connection()
3190
+ cursor = conn.cursor()
3191
+
3192
+ # Step 1: Validate order exists and status is "pending"
3193
+ cursor.execute("""
3194
+ SELECT status, delivery_lat, delivery_lng, delivery_address, assigned_driver_id
3195
+ FROM orders
3196
+ WHERE order_id = %s
3197
+ """, (order_id,))
3198
+
3199
+ order_row = cursor.fetchone()
3200
+ if not order_row:
3201
+ cursor.close()
3202
+ conn.close()
3203
+ return {
3204
+ "success": False,
3205
+ "error": f"Order not found: {order_id}"
3206
+ }
3207
+
3208
+ order_status = order_row['status']
3209
+ delivery_lat = order_row['delivery_lat']
3210
+ delivery_lng = order_row['delivery_lng']
3211
+ delivery_address = order_row['delivery_address']
3212
+ current_driver = order_row['assigned_driver_id']
3213
+
3214
+ if order_status != "pending":
3215
+ cursor.close()
3216
+ conn.close()
3217
+
3218
+ # Provide helpful error message based on current status
3219
+ if order_status == "assigned" and current_driver:
3220
+ # Get current driver name for better error message
3221
+ cursor2 = get_db_connection().cursor()
3222
+ cursor2.execute("SELECT name FROM drivers WHERE driver_id = %s", (current_driver,))
3223
+ driver_row = cursor2.fetchone()
3224
+ driver_name = driver_row['name'] if driver_row else current_driver
3225
+ cursor2.close()
3226
+
3227
+ return {
3228
+ "success": False,
3229
+ "error": f"Order {order_id} is already assigned to driver {driver_name}. Use 'unassign_order' first to reassign to a different driver."
3230
+ }
3231
+ else:
3232
+ return {
3233
+ "success": False,
3234
+ "error": f"Order must be in 'pending' status to be assigned. Current status: '{order_status}'"
3235
+ }
3236
+
3237
+ if not delivery_lat or not delivery_lng:
3238
+ cursor.close()
3239
+ conn.close()
3240
+ return {
3241
+ "success": False,
3242
+ "error": "Order does not have delivery location coordinates"
3243
+ }
3244
+
3245
+ # Step 2: Validate driver exists and status is "active"
3246
+ cursor.execute("""
3247
+ SELECT status, current_lat, current_lng, vehicle_type, name
3248
+ FROM drivers
3249
+ WHERE driver_id = %s
3250
+ """, (driver_id,))
3251
+
3252
+ driver_row = cursor.fetchone()
3253
+ if not driver_row:
3254
+ cursor.close()
3255
+ conn.close()
3256
+ return {
3257
+ "success": False,
3258
+ "error": f"Driver not found: {driver_id}"
3259
+ }
3260
+
3261
+ driver_status = driver_row['status']
3262
+ driver_lat = driver_row['current_lat']
3263
+ driver_lng = driver_row['current_lng']
3264
+ vehicle_type = driver_row['vehicle_type']
3265
+ driver_name = driver_row['name']
3266
+
3267
+ if driver_status not in ["active", "available"]:
3268
+ cursor.close()
3269
+ conn.close()
3270
+ return {
3271
+ "success": False,
3272
+ "error": f"Driver must be 'active' or 'available'. Current status: {driver_status}"
3273
+ }
3274
+
3275
+ if not driver_lat or not driver_lng:
3276
+ cursor.close()
3277
+ conn.close()
3278
+ return {
3279
+ "success": False,
3280
+ "error": "Driver does not have current location"
3281
+ }
3282
+
3283
+ # Step 3: Check if order already has active assignment
3284
+ cursor.execute("""
3285
+ SELECT assignment_id, driver_id
3286
+ FROM assignments
3287
+ WHERE order_id = %s AND status IN ('active', 'in_progress')
3288
+ """, (order_id,))
3289
+
3290
+ existing_assignment = cursor.fetchone()
3291
+ if existing_assignment:
3292
+ cursor.close()
3293
+ conn.close()
3294
+ existing_asn_id = existing_assignment['assignment_id']
3295
+ existing_driver_id = existing_assignment['driver_id']
3296
+
3297
+ # Get driver name for better error message
3298
+ cursor2 = get_db_connection().cursor()
3299
+ cursor2.execute("SELECT name FROM drivers WHERE driver_id = %s", (existing_driver_id,))
3300
+ driver_row = cursor2.fetchone()
3301
+ existing_driver_name = driver_row['name'] if driver_row else existing_driver_id
3302
+ cursor2.close()
3303
+
3304
+ return {
3305
+ "success": False,
3306
+ "error": f"Order {order_id} is already assigned to driver {existing_driver_name} (Assignment: {existing_asn_id}). Use 'unassign_order' first to reassign."
3307
+ }
3308
+
3309
+ # Step 4: Calculate route from driver location to delivery location
3310
+ logger.info(f"Calculating route: ({driver_lat},{driver_lng}) -> ({delivery_lat},{delivery_lng})")
3311
+
3312
+ route_result = handle_calculate_route({
3313
+ "origin": f"{driver_lat},{driver_lng}",
3314
+ "destination": f"{delivery_lat},{delivery_lng}",
3315
+ "vehicle_type": vehicle_type or "car",
3316
+ "alternatives": False,
3317
+ "include_steps": True # Get turn-by-turn directions
3318
+ })
3319
+
3320
+ if not route_result.get("success"):
3321
+ cursor.close()
3322
+ conn.close()
3323
+ return {
3324
+ "success": False,
3325
+ "error": f"Route calculation failed: {route_result.get('error', 'Unknown error')}"
3326
+ }
3327
+
3328
+ # Step 5: Generate assignment ID
3329
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
3330
+ assignment_id = f"ASN-{timestamp}"
3331
+
3332
+ # Step 6: Calculate estimated arrival
3333
+ duration_seconds = route_result.get("duration_in_traffic", {}).get("seconds", 0)
3334
+ estimated_arrival = datetime.now() + timedelta(seconds=duration_seconds)
3335
+
3336
+ # Step 7: Create assignment record
3337
+ import json
3338
+
3339
+ # Extract route directions (turn-by-turn steps)
3340
+ route_directions = route_result.get("steps", [])
3341
+ route_directions_json = json.dumps(route_directions) if route_directions else None
3342
+
3343
+ cursor.execute("""
3344
+ INSERT INTO assignments (
3345
+ assignment_id, order_id, driver_id,
3346
+ route_distance_meters, route_duration_seconds, route_duration_in_traffic_seconds,
3347
+ route_summary, route_confidence, route_directions,
3348
+ driver_start_location_lat, driver_start_location_lng,
3349
+ delivery_location_lat, delivery_location_lng, delivery_address,
3350
+ estimated_arrival, vehicle_type, traffic_delay_seconds,
3351
+ status
3352
+ ) VALUES (
3353
+ %s, %s, %s,
3354
+ %s, %s, %s,
3355
+ %s, %s, %s,
3356
+ %s, %s,
3357
+ %s, %s, %s,
3358
+ %s, %s, %s,
3359
+ %s
3360
+ )
3361
+ """, (
3362
+ assignment_id, order_id, driver_id,
3363
+ route_result.get("distance", {}).get("meters", 0),
3364
+ route_result.get("duration", {}).get("seconds", 0),
3365
+ route_result.get("duration_in_traffic", {}).get("seconds", 0),
3366
+ route_result.get("route_summary", ""),
3367
+ route_result.get("confidence", ""),
3368
+ route_directions_json,
3369
+ driver_lat, driver_lng,
3370
+ delivery_lat, delivery_lng, delivery_address,
3371
+ estimated_arrival, vehicle_type,
3372
+ route_result.get("traffic_delay", {}).get("seconds", 0),
3373
+ "active"
3374
+ ))
3375
+
3376
+ # Step 8: Update order status and assigned driver
3377
+ cursor.execute("""
3378
+ UPDATE orders
3379
+ SET status = 'assigned', assigned_driver_id = %s
3380
+ WHERE order_id = %s
3381
+ """, (driver_id, order_id))
3382
+
3383
+ # Step 9: Update driver status to busy
3384
+ cursor.execute("""
3385
+ UPDATE drivers
3386
+ SET status = 'busy'
3387
+ WHERE driver_id = %s
3388
+ """, (driver_id,))
3389
+
3390
+ conn.commit()
3391
+ cursor.close()
3392
+ conn.close()
3393
+
3394
+ logger.info(f"Assignment created successfully: {assignment_id}")
3395
+
3396
+ return {
3397
+ "success": True,
3398
+ "assignment_id": assignment_id,
3399
+ "order_id": order_id,
3400
+ "driver_id": driver_id,
3401
+ "driver_name": driver_name,
3402
+ "route": {
3403
+ "distance": route_result.get("distance", {}).get("text", ""),
3404
+ "duration": route_result.get("duration", {}).get("text", ""),
3405
+ "duration_in_traffic": route_result.get("duration_in_traffic", {}).get("text", ""),
3406
+ "traffic_delay": route_result.get("traffic_delay", {}).get("text", ""),
3407
+ "summary": route_result.get("route_summary", ""),
3408
+ "directions": route_directions # Turn-by-turn navigation steps
3409
+ },
3410
+ "estimated_arrival": estimated_arrival.isoformat(),
3411
+ "status": "active",
3412
+ "message": f"Order {order_id} assigned to driver {driver_name} ({driver_id})"
3413
+ }
3414
+
3415
+ except Exception as e:
3416
+ logger.error(f"Failed to create assignment: {e}")
3417
+ return {
3418
+ "success": False,
3419
+ "error": f"Failed to create assignment: {str(e)}"
3420
+ }
3421
+
3422
+
3423
+ def handle_get_assignment_details(tool_input: dict) -> dict:
3424
+ """
3425
+ Get assignment details
3426
+
3427
+ Can query by assignment_id, order_id, or driver_id.
3428
+ Returns assignment with route data and related order/driver info.
3429
+
3430
+ Args:
3431
+ tool_input: Dict with assignment_id, order_id, or driver_id
3432
+
3433
+ Returns:
3434
+ Assignment details or list of assignments
3435
+ """
3436
+ assignment_id = (tool_input.get("assignment_id") or "").strip()
3437
+ order_id = (tool_input.get("order_id") or "").strip()
3438
+ driver_id = (tool_input.get("driver_id") or "").strip()
3439
+
3440
+ if not assignment_id and not order_id and not driver_id:
3441
+ return {
3442
+ "success": False,
3443
+ "error": "Provide at least one of: assignment_id, order_id, or driver_id"
3444
+ }
3445
+
3446
+ try:
3447
+ conn = get_db_connection()
3448
+ cursor = conn.cursor()
3449
+
3450
+ # Build query based on provided parameters
3451
+ query = """
3452
+ SELECT
3453
+ a.assignment_id, a.order_id, a.driver_id, a.status,
3454
+ a.assigned_at, a.updated_at, a.estimated_arrival, a.actual_arrival,
3455
+ a.route_distance_meters, a.route_duration_seconds, a.route_duration_in_traffic_seconds,
3456
+ a.route_summary, a.route_confidence, a.traffic_delay_seconds, a.route_directions,
3457
+ a.driver_start_location_lat, a.driver_start_location_lng,
3458
+ a.delivery_location_lat, a.delivery_location_lng, a.delivery_address,
3459
+ a.vehicle_type, a.sequence_number, a.notes, a.failure_reason,
3460
+ o.customer_name, o.status as order_status,
3461
+ d.name as driver_name, d.status as driver_status, d.phone as driver_phone
3462
+ FROM assignments a
3463
+ LEFT JOIN orders o ON a.order_id = o.order_id
3464
+ LEFT JOIN drivers d ON a.driver_id = d.driver_id
3465
+ WHERE 1=1
3466
+ """
3467
+
3468
+ params = []
3469
+
3470
+ if assignment_id:
3471
+ query += " AND a.assignment_id = %s"
3472
+ params.append(assignment_id)
3473
+
3474
+ if order_id:
3475
+ query += " AND a.order_id = %s"
3476
+ params.append(order_id)
3477
+
3478
+ if driver_id:
3479
+ query += " AND a.driver_id = %s"
3480
+ params.append(driver_id)
3481
+
3482
+ query += " ORDER BY a.assigned_at DESC"
3483
+
3484
+ cursor.execute(query, params)
3485
+ rows = cursor.fetchall()
3486
+
3487
+ cursor.close()
3488
+ conn.close()
3489
+
3490
+ if not rows:
3491
+ return {
3492
+ "success": False,
3493
+ "error": "No assignments found matching criteria"
3494
+ }
3495
+
3496
+ # Format results
3497
+ assignments = []
3498
+ for row in rows:
3499
+ assignment = {
3500
+ "assignment_id": row['assignment_id'],
3501
+ "order_id": row['order_id'],
3502
+ "driver_id": row['driver_id'],
3503
+ "status": row['status'],
3504
+ "assigned_at": row['assigned_at'].isoformat() if row['assigned_at'] else None,
3505
+ "updated_at": row['updated_at'].isoformat() if row['updated_at'] else None,
3506
+ "estimated_arrival": row['estimated_arrival'].isoformat() if row['estimated_arrival'] else None,
3507
+ "actual_arrival": row['actual_arrival'].isoformat() if row['actual_arrival'] else None,
3508
+ "route": {
3509
+ "distance_meters": row['route_distance_meters'],
3510
+ "distance_km": round(row['route_distance_meters'] / 1000, 2) if row['route_distance_meters'] else 0,
3511
+ "duration_seconds": row['route_duration_seconds'],
3512
+ "duration_minutes": round(row['route_duration_seconds'] / 60, 1) if row['route_duration_seconds'] else 0,
3513
+ "duration_in_traffic_seconds": row['route_duration_in_traffic_seconds'],
3514
+ "duration_in_traffic_minutes": round(row['route_duration_in_traffic_seconds'] / 60, 1) if row['route_duration_in_traffic_seconds'] else 0,
3515
+ "summary": row['route_summary'],
3516
+ "confidence": row['route_confidence'],
3517
+ "traffic_delay_seconds": row['traffic_delay_seconds'],
3518
+ "traffic_delay_minutes": round(row['traffic_delay_seconds'] / 60, 1) if row['traffic_delay_seconds'] else 0,
3519
+ "directions": row['route_directions'] # Turn-by-turn navigation steps
3520
+ },
3521
+ "driver_start_location": {
3522
+ "lat": float(row['driver_start_location_lat']) if row['driver_start_location_lat'] else None,
3523
+ "lng": float(row['driver_start_location_lng']) if row['driver_start_location_lng'] else None
3524
+ },
3525
+ "delivery_location": {
3526
+ "lat": float(row['delivery_location_lat']) if row['delivery_location_lat'] else None,
3527
+ "lng": float(row['delivery_location_lng']) if row['delivery_location_lng'] else None,
3528
+ "address": row['delivery_address']
3529
+ },
3530
+ "vehicle_type": row['vehicle_type'],
3531
+ "sequence_number": row['sequence_number'],
3532
+ "notes": row['notes'],
3533
+ "failure_reason": row['failure_reason'],
3534
+ "order": {
3535
+ "customer_name": row['customer_name'],
3536
+ "status": row['order_status']
3537
+ },
3538
+ "driver": {
3539
+ "name": row['driver_name'],
3540
+ "status": row['driver_status'],
3541
+ "phone": row['driver_phone']
3542
+ }
3543
+ }
3544
+ assignments.append(assignment)
3545
+
3546
+ if assignment_id and len(assignments) == 1:
3547
+ # Single assignment query
3548
+ return {
3549
+ "success": True,
3550
+ "assignment": assignments[0]
3551
+ }
3552
+ else:
3553
+ # Multiple assignments
3554
+ return {
3555
+ "success": True,
3556
+ "count": len(assignments),
3557
+ "assignments": assignments
3558
+ }
3559
+
3560
+ except Exception as e:
3561
+ logger.error(f"Failed to get assignment details: {e}")
3562
+ return {
3563
+ "success": False,
3564
+ "error": f"Failed to get assignment details: {str(e)}"
3565
+ }
3566
+
3567
+
3568
+ def handle_update_assignment(tool_input: dict) -> dict:
3569
+ """
3570
+ Update assignment status
3571
+
3572
+ Allows updating assignment status and actual metrics.
3573
+ Manages cascading updates to order and driver statuses.
3574
+
3575
+ Args:
3576
+ tool_input: Dict with assignment_id, status (optional), actual_arrival (optional), notes (optional)
3577
+
3578
+ Returns:
3579
+ Update result
3580
+ """
3581
+ from datetime import datetime
3582
+
3583
+ assignment_id = (tool_input.get("assignment_id") or "").strip()
3584
+ new_status = (tool_input.get("status") or "").strip().lower()
3585
+ actual_arrival = tool_input.get("actual_arrival")
3586
+ notes = (tool_input.get("notes") or "").strip()
3587
+
3588
+ if not assignment_id:
3589
+ return {
3590
+ "success": False,
3591
+ "error": "assignment_id is required"
3592
+ }
3593
+
3594
+ if not new_status and not actual_arrival and not notes:
3595
+ return {
3596
+ "success": False,
3597
+ "error": "Provide at least one field to update: status, actual_arrival, or notes"
3598
+ }
3599
+
3600
+ # Validate status if provided
3601
+ valid_statuses = ["active", "in_progress", "completed", "failed", "cancelled"]
3602
+ if new_status and new_status not in valid_statuses:
3603
+ return {
3604
+ "success": False,
3605
+ "error": f"Invalid status. Must be one of: {', '.join(valid_statuses)}"
3606
+ }
3607
+
3608
+ logger.info(f"Updating assignment: {assignment_id}, status={new_status}")
3609
+
3610
+ try:
3611
+ conn = get_db_connection()
3612
+ cursor = conn.cursor()
3613
+
3614
+ # Get current assignment details
3615
+ cursor.execute("""
3616
+ SELECT status, order_id, driver_id
3617
+ FROM assignments
3618
+ WHERE assignment_id = %s
3619
+ """, (assignment_id,))
3620
+
3621
+ assignment_row = cursor.fetchone()
3622
+ if not assignment_row:
3623
+ cursor.close()
3624
+ conn.close()
3625
+ return {
3626
+ "success": False,
3627
+ "error": f"Assignment not found: {assignment_id}"
3628
+ }
3629
+
3630
+ current_status = assignment_row['status']
3631
+ order_id = assignment_row['order_id']
3632
+ driver_id = assignment_row['driver_id']
3633
+
3634
+ # Validate status transitions
3635
+ if new_status:
3636
+ # Cannot go backwards
3637
+ if current_status == "completed" and new_status in ["active", "in_progress"]:
3638
+ cursor.close()
3639
+ conn.close()
3640
+ return {
3641
+ "success": False,
3642
+ "error": "Cannot change status from 'completed' back to 'active' or 'in_progress'"
3643
+ }
3644
+
3645
+ if current_status == "failed" and new_status != "failed":
3646
+ cursor.close()
3647
+ conn.close()
3648
+ return {
3649
+ "success": False,
3650
+ "error": "Cannot change status from 'failed'"
3651
+ }
3652
+
3653
+ if current_status == "cancelled" and new_status != "cancelled":
3654
+ cursor.close()
3655
+ conn.close()
3656
+ return {
3657
+ "success": False,
3658
+ "error": "Cannot change status from 'cancelled'"
3659
+ }
3660
+
3661
+ # Build update query
3662
+ updates = []
3663
+ params = []
3664
+
3665
+ if new_status:
3666
+ updates.append("status = %s")
3667
+ params.append(new_status)
3668
+
3669
+ if actual_arrival:
3670
+ updates.append("actual_arrival = %s")
3671
+ params.append(actual_arrival)
3672
+
3673
+ if notes:
3674
+ updates.append("notes = %s")
3675
+ params.append(notes)
3676
+
3677
+ params.append(assignment_id)
3678
+
3679
+ # Update assignment
3680
+ cursor.execute(f"""
3681
+ UPDATE assignments
3682
+ SET {', '.join(updates)}
3683
+ WHERE assignment_id = %s
3684
+ """, params)
3685
+
3686
+ # Handle cascading updates based on new status
3687
+ if new_status:
3688
+ if new_status in ["completed", "failed", "cancelled"]:
3689
+ # Update order status
3690
+ if new_status == "completed":
3691
+ cursor.execute("""
3692
+ UPDATE orders
3693
+ SET status = 'delivered'
3694
+ WHERE order_id = %s
3695
+ """, (order_id,))
3696
+
3697
+ elif new_status == "failed":
3698
+ cursor.execute("""
3699
+ UPDATE orders
3700
+ SET status = 'failed'
3701
+ WHERE order_id = %s
3702
+ """, (order_id,))
3703
+
3704
+ elif new_status == "cancelled":
3705
+ cursor.execute("""
3706
+ UPDATE orders
3707
+ SET status = 'cancelled', assigned_driver_id = NULL
3708
+ WHERE order_id = %s
3709
+ """, (order_id,))
3710
+
3711
+ # Check if driver has other active assignments
3712
+ cursor.execute("""
3713
+ SELECT COUNT(*) as count
3714
+ FROM assignments
3715
+ WHERE driver_id = %s AND status IN ('active', 'in_progress') AND assignment_id != %s
3716
+ """, (driver_id, assignment_id))
3717
+
3718
+ other_assignments_count = cursor.fetchone()['count']
3719
+
3720
+ # If no other active assignments, set driver back to active
3721
+ if other_assignments_count == 0:
3722
+ cursor.execute("""
3723
+ UPDATE drivers
3724
+ SET status = 'active'
3725
+ WHERE driver_id = %s
3726
+ """, (driver_id,))
3727
+
3728
+ conn.commit()
3729
+ cursor.close()
3730
+ conn.close()
3731
+
3732
+ logger.info(f"Assignment updated successfully: {assignment_id}")
3733
+
3734
+ return {
3735
+ "success": True,
3736
+ "assignment_id": assignment_id,
3737
+ "updated_fields": {
3738
+ "status": new_status if new_status else current_status,
3739
+ "actual_arrival": actual_arrival if actual_arrival else "not updated",
3740
+ "notes": notes if notes else "not updated"
3741
+ },
3742
+ "message": f"Assignment {assignment_id} updated successfully"
3743
+ }
3744
+
3745
+ except Exception as e:
3746
+ logger.error(f"Failed to update assignment: {e}")
3747
+ return {
3748
+ "success": False,
3749
+ "error": f"Failed to update assignment: {str(e)}"
3750
+ }
3751
+
3752
+
3753
+ def handle_unassign_order(tool_input: dict) -> dict:
3754
+ """
3755
+ Unassign order (delete assignment)
3756
+
3757
+ Removes assignment and reverts order/driver to original states.
3758
+
3759
+ Args:
3760
+ tool_input: Dict with order_id or assignment_id, and confirm flag
3761
+
3762
+ Returns:
3763
+ Unassignment result
3764
+ """
3765
+ order_id = (tool_input.get("order_id") or "").strip()
3766
+ assignment_id = (tool_input.get("assignment_id") or "").strip()
3767
+ confirm = tool_input.get("confirm", False)
3768
+
3769
+ if not order_id and not assignment_id:
3770
+ return {
3771
+ "success": False,
3772
+ "error": "Provide either order_id or assignment_id"
3773
+ }
3774
+
3775
+ if not confirm:
3776
+ return {
3777
+ "success": False,
3778
+ "error": "Unassignment requires confirm=true for safety"
3779
+ }
3780
+
3781
+ logger.info(f"Unassigning: order_id={order_id}, assignment_id={assignment_id}")
3782
+
3783
+ try:
3784
+ conn = get_db_connection()
3785
+ cursor = conn.cursor()
3786
+
3787
+ # Find assignment
3788
+ if assignment_id:
3789
+ cursor.execute("""
3790
+ SELECT order_id, driver_id, status
3791
+ FROM assignments
3792
+ WHERE assignment_id = %s
3793
+ """, (assignment_id,))
3794
+ else:
3795
+ cursor.execute("""
3796
+ SELECT assignment_id, driver_id, status
3797
+ FROM assignments
3798
+ WHERE order_id = %s AND status IN ('active', 'in_progress')
3799
+ ORDER BY assigned_at DESC
3800
+ LIMIT 1
3801
+ """, (order_id,))
3802
+
3803
+ assignment_row = cursor.fetchone()
3804
+ if not assignment_row:
3805
+ cursor.close()
3806
+ conn.close()
3807
+ return {
3808
+ "success": False,
3809
+ "error": "No active assignment found"
3810
+ }
3811
+
3812
+ if assignment_id:
3813
+ found_order_id = assignment_row['order_id']
3814
+ driver_id = assignment_row['driver_id']
3815
+ status = assignment_row['status']
3816
+ else:
3817
+ assignment_id = assignment_row['assignment_id']
3818
+ driver_id = assignment_row['driver_id']
3819
+ status = assignment_row['status']
3820
+ found_order_id = order_id
3821
+
3822
+ # Validate status (cannot unassign if in_progress without force)
3823
+ if status == "in_progress":
3824
+ cursor.close()
3825
+ conn.close()
3826
+ return {
3827
+ "success": False,
3828
+ "error": "Cannot unassign order with 'in_progress' status. Complete or fail the delivery first."
3829
+ }
3830
+
3831
+ # Delete assignment
3832
+ cursor.execute("""
3833
+ DELETE FROM assignments
3834
+ WHERE assignment_id = %s
3835
+ """, (assignment_id,))
3836
+
3837
+ # Revert order status to pending and clear assigned driver
3838
+ cursor.execute("""
3839
+ UPDATE orders
3840
+ SET status = 'pending', assigned_driver_id = NULL
3841
+ WHERE order_id = %s
3842
+ """, (found_order_id,))
3843
+
3844
+ # Check if driver has other active assignments
3845
+ cursor.execute("""
3846
+ SELECT COUNT(*)
3847
+ FROM assignments
3848
+ WHERE driver_id = %s AND status IN ('active', 'in_progress')
3849
+ """, (driver_id,))
3850
+
3851
+ other_assignments_count = cursor.fetchone()[0]
3852
+
3853
+ # If no other active assignments, set driver back to active
3854
+ if other_assignments_count == 0:
3855
+ cursor.execute("""
3856
+ UPDATE drivers
3857
+ SET status = 'active'
3858
+ WHERE driver_id = %s
3859
+ """, (driver_id,))
3860
+
3861
+ conn.commit()
3862
+ cursor.close()
3863
+ conn.close()
3864
+
3865
+ logger.info(f"Assignment removed successfully: {assignment_id}")
3866
+
3867
+ return {
3868
+ "success": True,
3869
+ "assignment_id": assignment_id,
3870
+ "order_id": found_order_id,
3871
+ "driver_id": driver_id,
3872
+ "message": f"Order {found_order_id} unassigned from driver {driver_id}. Order status reverted to 'pending'."
3873
+ }
3874
+
3875
+ except Exception as e:
3876
+ logger.error(f"Failed to unassign order: {e}")
3877
+ return {
3878
+ "success": False,
3879
+ "error": f"Failed to unassign order: {str(e)}"
3880
+ }
3881
+
3882
+
3883
+ def handle_complete_delivery(tool_input: dict) -> dict:
3884
+ """
3885
+ Complete a delivery and automatically update driver location
3886
+
3887
+ Marks delivery as completed, updates order/driver statuses, and moves
3888
+ driver location to the delivery address.
3889
+
3890
+ Args:
3891
+ tool_input: Dict with assignment_id, confirm flag, and optional fields
3892
+
3893
+ Returns:
3894
+ Completion result
3895
+ """
3896
+ from datetime import datetime
3897
+
3898
+ assignment_id = (tool_input.get("assignment_id") or "").strip()
3899
+ confirm = tool_input.get("confirm", False)
3900
+ actual_distance_meters = tool_input.get("actual_distance_meters")
3901
+ notes = (tool_input.get("notes") or "").strip()
3902
+
3903
+ if not assignment_id:
3904
+ return {
3905
+ "success": False,
3906
+ "error": "assignment_id is required"
3907
+ }
3908
+
3909
+ if not confirm:
3910
+ return {
3911
+ "success": False,
3912
+ "error": "Delivery completion requires confirm=true for safety"
3913
+ }
3914
+
3915
+ logger.info(f"Completing delivery: assignment_id={assignment_id}")
3916
+
3917
+ try:
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
3930
+ JOIN drivers d ON a.driver_id = d.driver_id
3931
+ WHERE a.assignment_id = %s
3932
+ """, (assignment_id,))
3933
+
3934
+ assignment_row = cursor.fetchone()
3935
+ if not assignment_row:
3936
+ cursor.close()
3937
+ conn.close()
3938
+ return {
3939
+ "success": False,
3940
+ "error": f"Assignment not found: {assignment_id}"
3941
+ }
3942
+
3943
+ status = assignment_row['status']
3944
+ order_id = assignment_row['order_id']
3945
+ driver_id = assignment_row['driver_id']
3946
+ delivery_lat = assignment_row['delivery_location_lat']
3947
+ delivery_lng = assignment_row['delivery_location_lng']
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"]:
3954
+ cursor.close()
3955
+ conn.close()
3956
+ return {
3957
+ "success": False,
3958
+ "error": f"Cannot complete delivery: assignment status is '{status}'. Must be 'active' or 'in_progress'."
3959
+ }
3960
+
3961
+ # Validate delivery location exists
3962
+ if not delivery_lat or not delivery_lng:
3963
+ cursor.close()
3964
+ conn.close()
3965
+ return {
3966
+ "success": False,
3967
+ "error": "Cannot complete delivery: delivery location coordinates are missing"
3968
+ }
3969
+
3970
+ # Current timestamp for completion
3971
+ completion_time = datetime.now()
3972
+
3973
+ # Step 1: Update assignment to completed
3974
+ update_fields = ["status = %s", "actual_arrival = %s", "updated_at = %s"]
3975
+ params = ["completed", completion_time, completion_time]
3976
+
3977
+ if actual_distance_meters:
3978
+ update_fields.append("actual_distance_meters = %s")
3979
+ params.append(actual_distance_meters)
3980
+
3981
+ if notes:
3982
+ update_fields.append("notes = %s")
3983
+ params.append(notes)
3984
+
3985
+ params.append(assignment_id)
3986
+
3987
+ cursor.execute(f"""
3988
+ UPDATE assignments
3989
+ SET {', '.join(update_fields)}
3990
+ WHERE assignment_id = %s
3991
+ """, tuple(params))
3992
+
3993
+ # Step 2: Update driver location to delivery address
3994
+ cursor.execute("""
3995
+ UPDATE drivers
3996
+ SET current_lat = %s,
3997
+ current_lng = %s,
3998
+ last_location_update = %s,
3999
+ updated_at = %s
4000
+ WHERE driver_id = %s
4001
+ """, (delivery_lat, delivery_lng, completion_time, completion_time, driver_id))
4002
+
4003
+ logger.info(f"Driver {driver_id} location updated to delivery address: ({delivery_lat}, {delivery_lng})")
4004
+
4005
+ # Step 3: Update order status to delivered
4006
+ cursor.execute("""
4007
+ UPDATE orders
4008
+ SET status = 'delivered', updated_at = %s
4009
+ WHERE order_id = %s
4010
+ """, (completion_time, order_id))
4011
+
4012
+ # Step 4: Check if driver has other active assignments
4013
+ cursor.execute("""
4014
+ SELECT COUNT(*) as count FROM assignments
4015
+ WHERE driver_id = %s AND status IN ('active', 'in_progress')
4016
+ AND assignment_id != %s
4017
+ """, (driver_id, assignment_id))
4018
+
4019
+ other_assignments_count = cursor.fetchone()['count']
4020
+
4021
+ # Step 5: If no other active assignments, set driver to active
4022
+ cascading_actions = []
4023
+ if other_assignments_count == 0:
4024
+ cursor.execute("""
4025
+ UPDATE drivers
4026
+ SET status = 'active', updated_at = %s
4027
+ WHERE driver_id = %s
4028
+ """, (completion_time, driver_id))
4029
+ cascading_actions.append(f"Driver {driver_name} set to 'active' (no other assignments)")
4030
+ else:
4031
+ cascading_actions.append(f"Driver {driver_name} remains 'busy' ({other_assignments_count} other active assignment(s))")
4032
+
4033
+ conn.commit()
4034
+ cursor.close()
4035
+ conn.close()
4036
+
4037
+ logger.info(f"Delivery completed successfully: {assignment_id}")
4038
+
4039
+ return {
4040
+ "success": True,
4041
+ "assignment_id": assignment_id,
4042
+ "order_id": order_id,
4043
+ "driver_id": driver_id,
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),
4050
+ "address": delivery_address
4051
+ },
4052
+ "driver_updated": {
4053
+ "new_location": f"{delivery_lat}, {delivery_lng}",
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:
4061
+ logger.error(f"Failed to complete delivery: {e}")
4062
+ return {
4063
+ "success": False,
4064
+ "error": f"Failed to complete delivery: {str(e)}"
4065
+ }
4066
+
4067
+
4068
+ def handle_fail_delivery(tool_input: dict) -> dict:
4069
+ """
4070
+ Mark delivery as failed with mandatory location and reason
4071
+
4072
+ Driver must provide current GPS location and failure reason.
4073
+ Updates driver location to reported coordinates and sets statuses accordingly.
4074
+
4075
+ Args:
4076
+ tool_input: Dict with assignment_id, current_lat, current_lng, failure_reason,
4077
+ confirm flag, and optional notes
4078
+
4079
+ Returns:
4080
+ Failure recording result
4081
+ """
4082
+ from datetime import datetime
4083
+
4084
+ assignment_id = (tool_input.get("assignment_id") or "").strip()
4085
+ current_lat = tool_input.get("current_lat")
4086
+ current_lng = tool_input.get("current_lng")
4087
+ failure_reason = (tool_input.get("failure_reason") or "").strip()
4088
+ confirm = tool_input.get("confirm", False)
4089
+ notes = (tool_input.get("notes") or "").strip()
4090
+
4091
+ # Validation
4092
+ if not assignment_id:
4093
+ return {
4094
+ "success": False,
4095
+ "error": "assignment_id is required"
4096
+ }
4097
+
4098
+ if not confirm:
4099
+ return {
4100
+ "success": False,
4101
+ "error": "Delivery failure requires confirm=true for safety"
4102
+ }
4103
+
4104
+ if current_lat is None or current_lng is None:
4105
+ return {
4106
+ "success": False,
4107
+ "error": "Driver must provide current location (current_lat and current_lng required)"
4108
+ }
4109
+
4110
+ if not failure_reason:
4111
+ return {
4112
+ "success": False,
4113
+ "error": "Failure reason is required. Valid reasons: customer_not_available, wrong_address, refused_delivery, damaged_goods, payment_issue, vehicle_breakdown, access_restricted, weather_conditions, other"
4114
+ }
4115
+
4116
+ # Validate failure_reason is one of the allowed values
4117
+ valid_reasons = [
4118
+ "customer_not_available",
4119
+ "wrong_address",
4120
+ "refused_delivery",
4121
+ "damaged_goods",
4122
+ "payment_issue",
4123
+ "vehicle_breakdown",
4124
+ "access_restricted",
4125
+ "weather_conditions",
4126
+ "other"
4127
+ ]
4128
+
4129
+ if failure_reason not in valid_reasons:
4130
+ return {
4131
+ "success": False,
4132
+ "error": f"Invalid failure_reason '{failure_reason}'. Must be one of: {', '.join(valid_reasons)}"
4133
+ }
4134
+
4135
+ # Validate coordinates are valid
4136
+ try:
4137
+ current_lat = float(current_lat)
4138
+ current_lng = float(current_lng)
4139
+ if not (-90 <= current_lat <= 90) or not (-180 <= current_lng <= 180):
4140
+ return {
4141
+ "success": False,
4142
+ "error": "Invalid GPS coordinates. Latitude must be -90 to 90, longitude must be -180 to 180"
4143
+ }
4144
+ except (ValueError, TypeError):
4145
+ return {
4146
+ "success": False,
4147
+ "error": "current_lat and current_lng must be valid numbers"
4148
+ }
4149
+
4150
+ logger.info(f"Failing delivery: assignment_id={assignment_id}, reason={failure_reason}")
4151
+
4152
+ try:
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
4165
+ JOIN drivers d ON a.driver_id = d.driver_id
4166
+ WHERE a.assignment_id = %s
4167
+ """, (assignment_id,))
4168
+
4169
+ assignment_row = cursor.fetchone()
4170
+ if not assignment_row:
4171
+ cursor.close()
4172
+ conn.close()
4173
+ return {
4174
+ "success": False,
4175
+ "error": f"Assignment not found: {assignment_id}"
4176
+ }
4177
+
4178
+ status = assignment_row['status']
4179
+ order_id = assignment_row['order_id']
4180
+ driver_id = assignment_row['driver_id']
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"]:
4187
+ cursor.close()
4188
+ conn.close()
4189
+ return {
4190
+ "success": False,
4191
+ "error": f"Cannot fail delivery: assignment status is '{status}'. Must be 'active' or 'in_progress'."
4192
+ }
4193
+
4194
+ # Current timestamp for failure
4195
+ failure_time = datetime.now()
4196
+
4197
+ # Step 1: Update assignment to failed
4198
+ update_fields = [
4199
+ "status = %s",
4200
+ "failure_reason = %s",
4201
+ "actual_arrival = %s",
4202
+ "updated_at = %s"
4203
+ ]
4204
+ params = ["failed", failure_reason, failure_time, failure_time]
4205
+
4206
+ if notes:
4207
+ update_fields.append("notes = %s")
4208
+ params.append(notes)
4209
+
4210
+ params.append(assignment_id)
4211
+
4212
+ cursor.execute(f"""
4213
+ UPDATE assignments
4214
+ SET {', '.join(update_fields)}
4215
+ WHERE assignment_id = %s
4216
+ """, tuple(params))
4217
+
4218
+ # Step 2: Update driver location to reported current location
4219
+ cursor.execute("""
4220
+ UPDATE drivers
4221
+ SET current_lat = %s,
4222
+ current_lng = %s,
4223
+ last_location_update = %s,
4224
+ updated_at = %s
4225
+ WHERE driver_id = %s
4226
+ """, (current_lat, current_lng, failure_time, failure_time, driver_id))
4227
+
4228
+ logger.info(f"Driver {driver_id} location updated to reported position: ({current_lat}, {current_lng})")
4229
+
4230
+ # Step 3: Update order status to failed
4231
+ cursor.execute("""
4232
+ UPDATE orders
4233
+ SET status = 'failed', updated_at = %s
4234
+ WHERE order_id = %s
4235
+ """, (failure_time, order_id))
4236
+
4237
+ # Step 4: Check if driver has other active assignments
4238
+ cursor.execute("""
4239
+ SELECT COUNT(*) as count FROM assignments
4240
+ WHERE driver_id = %s AND status IN ('active', 'in_progress')
4241
+ AND assignment_id != %s
4242
+ """, (driver_id, assignment_id))
4243
+
4244
+ other_assignments_count = cursor.fetchone()['count']
4245
+
4246
+ # Step 5: If no other active assignments, set driver to active
4247
+ cascading_actions = []
4248
+ if other_assignments_count == 0:
4249
+ cursor.execute("""
4250
+ UPDATE drivers
4251
+ SET status = 'active', updated_at = %s
4252
+ WHERE driver_id = %s
4253
+ """, (failure_time, driver_id))
4254
+ cascading_actions.append(f"Driver {driver_name} set to 'active' (no other assignments)")
4255
+ else:
4256
+ cascading_actions.append(f"Driver {driver_name} remains 'busy' ({other_assignments_count} other active assignment(s))")
4257
+
4258
+ conn.commit()
4259
+ cursor.close()
4260
+ conn.close()
4261
+
4262
+ logger.info(f"Delivery marked as failed: {assignment_id}")
4263
+
4264
+ # Format failure reason for display
4265
+ reason_display = failure_reason.replace("_", " ").title()
4266
+
4267
+ return {
4268
+ "success": True,
4269
+ "assignment_id": assignment_id,
4270
+ "order_id": order_id,
4271
+ "driver_id": driver_id,
4272
+ "customer_name": customer_name,
4273
+ "driver_name": driver_name,
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,
4280
+ "lng": current_lng,
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:
4288
+ logger.error(f"Failed to record delivery failure: {e}")
4289
+ return {
4290
+ "success": False,
4291
+ "error": f"Failed to record delivery failure: {str(e)}"
4292
+ }
4293
+
4294
+
4295
  def get_tools_list() -> list:
4296
  """Get list of available tools"""
4297
  return [tool["name"] for tool in TOOLS_SCHEMA]
database/migrations/002_create_assignments_table.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Migration 002: Create Assignments Table
3
+ Creates the assignments table for managing order-driver assignments with route data
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
+ -- Create assignments table with enhanced route tracking
16
+ CREATE TABLE IF NOT EXISTS assignments (
17
+ assignment_id VARCHAR(50) PRIMARY KEY,
18
+ order_id VARCHAR(50) NOT NULL,
19
+ driver_id VARCHAR(50) NOT NULL,
20
+
21
+ -- Assignment metadata
22
+ assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
24
+ sequence_number INTEGER,
25
+
26
+ -- Route data from Google Routes API
27
+ route_distance_meters INTEGER,
28
+ route_duration_seconds INTEGER,
29
+ route_duration_in_traffic_seconds INTEGER,
30
+ route_summary TEXT,
31
+ route_polyline TEXT,
32
+
33
+ -- Origin/destination tracking
34
+ driver_start_location_lat DECIMAL(10, 8),
35
+ driver_start_location_lng DECIMAL(11, 8),
36
+ delivery_location_lat DECIMAL(10, 8),
37
+ delivery_location_lng DECIMAL(11, 8),
38
+ delivery_address TEXT,
39
+
40
+ -- Timing data
41
+ estimated_arrival TIMESTAMP,
42
+ actual_arrival TIMESTAMP,
43
+
44
+ -- Actual vs estimated tracking
45
+ actual_distance_meters INTEGER,
46
+
47
+ -- Vehicle context
48
+ vehicle_type VARCHAR(50),
49
+
50
+ -- Status management
51
+ status VARCHAR(20) CHECK(status IN ('active', 'in_progress', 'completed', 'cancelled', 'failed')) DEFAULT 'active',
52
+
53
+ -- Additional metadata
54
+ traffic_delay_seconds INTEGER,
55
+ weather_conditions JSONB,
56
+ route_confidence VARCHAR(100),
57
+ notes TEXT,
58
+
59
+ -- Foreign keys with proper cascade behavior
60
+ FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
61
+ FOREIGN KEY (driver_id) REFERENCES drivers(driver_id) ON DELETE RESTRICT
62
+ );
63
+
64
+ -- Create indexes for performance
65
+ CREATE INDEX IF NOT EXISTS idx_assignments_driver ON assignments(driver_id);
66
+ CREATE INDEX IF NOT EXISTS idx_assignments_order ON assignments(order_id);
67
+ CREATE INDEX IF NOT EXISTS idx_assignments_status ON assignments(status);
68
+ CREATE INDEX IF NOT EXISTS idx_assignments_assigned_at ON assignments(assigned_at);
69
+
70
+ -- Create trigger for auto-updating updated_at
71
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
72
+ RETURNS TRIGGER AS $$
73
+ BEGIN
74
+ NEW.updated_at = CURRENT_TIMESTAMP;
75
+ RETURN NEW;
76
+ END;
77
+ $$ LANGUAGE plpgsql;
78
+
79
+ CREATE TRIGGER update_assignments_timestamp
80
+ BEFORE UPDATE ON assignments
81
+ FOR EACH ROW
82
+ EXECUTE FUNCTION update_updated_at_column();
83
+
84
+ -- Add unique constraint to prevent multiple active assignments per order
85
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_unique_active_order
86
+ ON assignments(order_id)
87
+ WHERE status IN ('active', 'in_progress');
88
+ """
89
+
90
+ ROLLBACK_SQL = """
91
+ DROP INDEX IF EXISTS idx_assignments_unique_active_order;
92
+ DROP TRIGGER IF EXISTS update_assignments_timestamp ON assignments;
93
+ DROP INDEX IF EXISTS idx_assignments_assigned_at;
94
+ DROP INDEX IF EXISTS idx_assignments_status;
95
+ DROP INDEX IF EXISTS idx_assignments_order;
96
+ DROP INDEX IF EXISTS idx_assignments_driver;
97
+ DROP TABLE IF EXISTS assignments CASCADE;
98
+ """
99
+
100
+
101
+ def up():
102
+ """Apply migration - create assignments table"""
103
+ print("Running migration 002: Create assignments table...")
104
+
105
+ try:
106
+ conn = get_db_connection()
107
+ cursor = conn.cursor()
108
+
109
+ # Execute migration SQL
110
+ cursor.execute(MIGRATION_SQL)
111
+
112
+ conn.commit()
113
+ cursor.close()
114
+ conn.close()
115
+
116
+ print("SUCCESS: Migration 002 applied successfully")
117
+ print(" - Created assignments table")
118
+ print(" - Created indexes (driver, order, status, assigned_at)")
119
+ print(" - Created update trigger")
120
+ print(" - Created unique constraint for active assignments per order")
121
+ return True
122
+
123
+ except Exception as e:
124
+ print(f"ERROR: Migration 002 failed: {e}")
125
+ return False
126
+
127
+
128
+ def down():
129
+ """Rollback migration - drop assignments table"""
130
+ print("Rolling back migration 002: Drop assignments table...")
131
+
132
+ try:
133
+ conn = get_db_connection()
134
+ cursor = conn.cursor()
135
+
136
+ # Execute rollback SQL
137
+ cursor.execute(ROLLBACK_SQL)
138
+
139
+ conn.commit()
140
+ cursor.close()
141
+ conn.close()
142
+
143
+ print("SUCCESS: Migration 002 rolled back successfully")
144
+ return True
145
+
146
+ except Exception as e:
147
+ print(f"ERROR: Migration 002 rollback failed: {e}")
148
+ return False
149
+
150
+
151
+ if __name__ == "__main__":
152
+ import sys
153
+
154
+ if len(sys.argv) > 1 and sys.argv[1] == "down":
155
+ down()
156
+ else:
157
+ up()
database/migrations/003_add_order_driver_fk.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Migration 003: Add Foreign Key Constraint to orders.assigned_driver_id
3
+ Adds FK constraint to ensure referential integrity between orders and drivers
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 foreign key constraint to orders.assigned_driver_id
16
+ -- Use ON DELETE SET NULL so that if driver is deleted, order is not lost
17
+ -- (The assignment record will be handled separately via RESTRICT constraint)
18
+ ALTER TABLE orders
19
+ ADD CONSTRAINT fk_orders_assigned_driver
20
+ FOREIGN KEY (assigned_driver_id)
21
+ REFERENCES drivers(driver_id)
22
+ ON DELETE SET NULL;
23
+ """
24
+
25
+ ROLLBACK_SQL = """
26
+ -- Drop foreign key constraint
27
+ ALTER TABLE orders
28
+ DROP CONSTRAINT IF EXISTS fk_orders_assigned_driver;
29
+ """
30
+
31
+
32
+ def up():
33
+ """Apply migration - add FK constraint"""
34
+ print("Running migration 003: Add FK constraint to orders.assigned_driver_id...")
35
+
36
+ try:
37
+ conn = get_db_connection()
38
+ cursor = conn.cursor()
39
+
40
+ # Execute migration SQL
41
+ cursor.execute(MIGRATION_SQL)
42
+
43
+ conn.commit()
44
+ cursor.close()
45
+ conn.close()
46
+
47
+ print("SUCCESS: Migration 003 applied successfully")
48
+ print(" - Added FK constraint: orders.assigned_driver_id -> drivers.driver_id")
49
+ print(" - ON DELETE SET NULL (preserves order history if driver deleted)")
50
+ return True
51
+
52
+ except Exception as e:
53
+ print(f"ERROR: Migration 003 failed: {e}")
54
+ print(" Note: This may fail if there are existing invalid driver references")
55
+ print(" Clean up orphaned assigned_driver_id values before running this migration")
56
+ return False
57
+
58
+
59
+ def down():
60
+ """Rollback migration - drop FK constraint"""
61
+ print("Rolling back migration 003: Drop FK constraint from orders.assigned_driver_id...")
62
+
63
+ try:
64
+ conn = get_db_connection()
65
+ cursor = conn.cursor()
66
+
67
+ # Execute rollback SQL
68
+ cursor.execute(ROLLBACK_SQL)
69
+
70
+ conn.commit()
71
+ cursor.close()
72
+ conn.close()
73
+
74
+ print("SUCCESS: Migration 003 rolled back successfully")
75
+ return True
76
+
77
+ except Exception as e:
78
+ print(f"ERROR: Migration 003 rollback failed: {e}")
79
+ return False
80
+
81
+
82
+ if __name__ == "__main__":
83
+ import sys
84
+
85
+ if len(sys.argv) > 1 and sys.argv[1] == "down":
86
+ down()
87
+ else:
88
+ up()
database/migrations/004_add_route_directions.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Migration 004: Add route_directions column to assignments table
3
+ Adds JSONB column to store turn-by-turn navigation instructions from Google Routes API
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 route_directions column to store turn-by-turn navigation steps
16
+ ALTER TABLE assignments
17
+ ADD COLUMN IF NOT EXISTS route_directions JSONB;
18
+
19
+ -- Add comment to explain the column
20
+ COMMENT ON COLUMN assignments.route_directions IS 'Turn-by-turn navigation instructions from Google Routes API (array of steps with instructions, distance, duration)';
21
+ """
22
+
23
+ ROLLBACK_SQL = """
24
+ -- Drop route_directions column
25
+ ALTER TABLE assignments
26
+ DROP COLUMN IF EXISTS route_directions;
27
+ """
28
+
29
+
30
+ def up():
31
+ """Apply migration - add route_directions column"""
32
+ print("Running migration 004: Add route_directions column to assignments table...")
33
+
34
+ try:
35
+ conn = get_db_connection()
36
+ cursor = conn.cursor()
37
+
38
+ # Execute migration SQL
39
+ cursor.execute(MIGRATION_SQL)
40
+
41
+ conn.commit()
42
+ cursor.close()
43
+ conn.close()
44
+
45
+ print("SUCCESS: Migration 004 applied successfully")
46
+ print(" - Added route_directions JSONB column to assignments table")
47
+ print(" - Column will store turn-by-turn navigation instructions")
48
+ return True
49
+
50
+ except Exception as e:
51
+ print(f"ERROR: Migration 004 failed: {e}")
52
+ return False
53
+
54
+
55
+ def down():
56
+ """Rollback migration - drop route_directions column"""
57
+ print("Rolling back migration 004: Drop route_directions column...")
58
+
59
+ try:
60
+ conn = get_db_connection()
61
+ cursor = conn.cursor()
62
+
63
+ # Execute rollback SQL
64
+ cursor.execute(ROLLBACK_SQL)
65
+
66
+ conn.commit()
67
+ cursor.close()
68
+ conn.close()
69
+
70
+ print("SUCCESS: Migration 004 rolled back successfully")
71
+ return True
72
+
73
+ except Exception as e:
74
+ print(f"ERROR: Migration 004 rollback failed: {e}")
75
+ return False
76
+
77
+
78
+ if __name__ == "__main__":
79
+ import sys
80
+
81
+ if len(sys.argv) > 1 and sys.argv[1] == "down":
82
+ down()
83
+ else:
84
+ up()
database/migrations/005_add_failure_reason.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Migration 005: Add failure_reason column to assignments table
3
+ Adds structured failure reason field for failed deliveries
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 failure_reason column with predefined categories
16
+ ALTER TABLE assignments
17
+ ADD COLUMN IF NOT EXISTS failure_reason VARCHAR(100)
18
+ CHECK(failure_reason IN (
19
+ 'customer_not_available',
20
+ 'wrong_address',
21
+ 'refused_delivery',
22
+ 'damaged_goods',
23
+ 'payment_issue',
24
+ 'vehicle_breakdown',
25
+ 'access_restricted',
26
+ 'weather_conditions',
27
+ 'other'
28
+ ));
29
+
30
+ -- Add comment to explain the column
31
+ COMMENT ON COLUMN assignments.failure_reason IS 'Structured reason for delivery failure (required when status is failed)';
32
+ """
33
+
34
+ ROLLBACK_SQL = """
35
+ -- Drop failure_reason column
36
+ ALTER TABLE assignments
37
+ DROP COLUMN IF EXISTS failure_reason;
38
+ """
39
+
40
+
41
+ def up():
42
+ """Apply migration - add failure_reason column"""
43
+ print("Running migration 005: Add failure_reason column to assignments table...")
44
+
45
+ try:
46
+ conn = get_db_connection()
47
+ cursor = conn.cursor()
48
+
49
+ # Execute migration SQL
50
+ cursor.execute(MIGRATION_SQL)
51
+
52
+ conn.commit()
53
+ cursor.close()
54
+ conn.close()
55
+
56
+ print("SUCCESS: Migration 005 applied successfully")
57
+ print(" - Added failure_reason VARCHAR(100) column to assignments table")
58
+ print(" - Constraint added for predefined failure categories")
59
+ print(" - Available reasons: customer_not_available, wrong_address, refused_delivery,")
60
+ print(" damaged_goods, payment_issue, vehicle_breakdown, access_restricted,")
61
+ print(" weather_conditions, other")
62
+ return True
63
+
64
+ except Exception as e:
65
+ print(f"ERROR: Migration 005 failed: {e}")
66
+ return False
67
+
68
+
69
+ def down():
70
+ """Rollback migration - drop failure_reason column"""
71
+ print("Rolling back migration 005: Drop failure_reason column...")
72
+
73
+ try:
74
+ conn = get_db_connection()
75
+ cursor = conn.cursor()
76
+
77
+ # Execute rollback SQL
78
+ cursor.execute(ROLLBACK_SQL)
79
+
80
+ conn.commit()
81
+ cursor.close()
82
+ conn.close()
83
+
84
+ print("SUCCESS: Migration 005 rolled back successfully")
85
+ return True
86
+
87
+ except Exception as e:
88
+ print(f"ERROR: Migration 005 rollback failed: {e}")
89
+ return False
90
+
91
+
92
+ if __name__ == "__main__":
93
+ import sys
94
+
95
+ if len(sys.argv) > 1 and sys.argv[1] == "down":
96
+ down()
97
+ else:
98
+ up()
server.py CHANGED
@@ -935,6 +935,362 @@ def delete_driver(driver_id: str, confirm: bool) -> dict:
935
  return handle_delete_driver({"driver_id": driver_id, "confirm": confirm})
936
 
937
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
  # ============================================================================
939
  # MAIN ENTRY POINT
940
  # ============================================================================
@@ -944,7 +1300,7 @@ if __name__ == "__main__":
944
  logger.info("FleetMind MCP Server v1.0.0")
945
  logger.info("=" * 60)
946
  logger.info(f"Geocoding: {geocoding_service.get_status()}")
947
- logger.info("Tools: 18 tools registered")
948
  logger.info("Resources: 2 resources available")
949
  logger.info("Prompts: 3 workflow templates")
950
  logger.info("Starting MCP server...")
 
935
  return handle_delete_driver({"driver_id": driver_id, "confirm": confirm})
936
 
937
 
938
+ @mcp.tool()
939
+ def delete_all_orders(confirm: bool, status: str = None) -> dict:
940
+ """
941
+ Bulk delete all orders (or orders with specific status). DANGEROUS - Use with extreme caution!
942
+
943
+ Safety checks:
944
+ - Requires confirm=true
945
+ - Blocks deletion if any active assignments exist
946
+ - Optional status filter to delete only specific statuses
947
+
948
+ Args:
949
+ confirm: Must be set to true to confirm bulk deletion
950
+ status: Optional status filter (pending/assigned/in_transit/delivered/failed/cancelled)
951
+
952
+ Returns:
953
+ dict: {
954
+ success: bool,
955
+ deleted_count: int,
956
+ message: str
957
+ }
958
+ """
959
+ from chat.tools import handle_delete_all_orders
960
+ logger.info(f"Tool: delete_all_orders(confirm={confirm}, status='{status}')")
961
+ return handle_delete_all_orders({"confirm": confirm, "status": status})
962
+
963
+
964
+ @mcp.tool()
965
+ def delete_all_drivers(confirm: bool, status: str = None) -> dict:
966
+ """
967
+ Bulk delete all drivers (or drivers with specific status). DANGEROUS - Use with extreme caution!
968
+
969
+ Safety checks:
970
+ - Requires confirm=true
971
+ - Blocks deletion if ANY assignments exist (due to RESTRICT constraint)
972
+ - Optional status filter to delete only specific statuses
973
+
974
+ Args:
975
+ confirm: Must be set to true to confirm bulk deletion
976
+ status: Optional status filter (active/busy/offline/unavailable)
977
+
978
+ Returns:
979
+ dict: {
980
+ success: bool,
981
+ deleted_count: int,
982
+ message: str
983
+ }
984
+ """
985
+ from chat.tools import handle_delete_all_drivers
986
+ logger.info(f"Tool: delete_all_drivers(confirm={confirm}, status='{status}')")
987
+ return handle_delete_all_drivers({"confirm": confirm, "status": status})
988
+
989
+
990
+ # ============================================================================
991
+ # ASSIGNMENT TOOLS
992
+ # ============================================================================
993
+
994
+ @mcp.tool()
995
+ def create_assignment(order_id: str, driver_id: str) -> dict:
996
+ """
997
+ Assign an order to a driver. Creates an assignment record with route data from driver location to delivery location.
998
+
999
+ Requirements:
1000
+ - Order must be in 'pending' status
1001
+ - Driver must be in 'active' or 'available' status
1002
+ - Order cannot already have an active assignment
1003
+
1004
+ After assignment:
1005
+ - Order status changes to 'assigned'
1006
+ - Driver status changes to 'busy'
1007
+ - Route data (distance, duration, path) is calculated and saved
1008
+ - Assignment record is created with all route details
1009
+
1010
+ Args:
1011
+ order_id: Order ID to assign (e.g., 'ORD-20250114123456')
1012
+ driver_id: Driver ID to assign (e.g., 'DRV-20250114123456')
1013
+
1014
+ Returns:
1015
+ dict: {
1016
+ success: bool,
1017
+ assignment_id: str,
1018
+ order_id: str,
1019
+ driver_id: str,
1020
+ route: {
1021
+ distance: {meters: int, text: str},
1022
+ duration: {seconds: int, text: str},
1023
+ route_summary: str,
1024
+ driver_start: {lat: float, lng: float},
1025
+ delivery_location: {lat: float, lng: float, address: str}
1026
+ }
1027
+ }
1028
+ """
1029
+ from chat.tools import handle_create_assignment
1030
+ logger.info(f"Tool: create_assignment(order_id='{order_id}', driver_id='{driver_id}')")
1031
+ return handle_create_assignment({"order_id": order_id, "driver_id": driver_id})
1032
+
1033
+
1034
+ @mcp.tool()
1035
+ def get_assignment_details(
1036
+ assignment_id: str = None,
1037
+ order_id: str = None,
1038
+ driver_id: str = None
1039
+ ) -> dict:
1040
+ """
1041
+ Get assignment details by assignment ID, order ID, or driver ID.
1042
+ Provide at least one parameter to search.
1043
+
1044
+ Args:
1045
+ assignment_id: Assignment ID (e.g., 'ASN-20250114123456')
1046
+ order_id: Order ID to find assignments for (e.g., 'ORD-20250114123456')
1047
+ driver_id: Driver ID to find assignments for (e.g., 'DRV-20250114123456')
1048
+
1049
+ Returns:
1050
+ dict: {
1051
+ success: bool,
1052
+ assignments: [
1053
+ {
1054
+ assignment_id: str,
1055
+ order_id: str,
1056
+ driver_id: str,
1057
+ customer_name: str,
1058
+ driver_name: str,
1059
+ status: str,
1060
+ route_distance_meters: int,
1061
+ route_duration_seconds: int,
1062
+ route_summary: str,
1063
+ driver_start_location: {lat: float, lng: float},
1064
+ delivery_location: {lat: float, lng: float, address: str},
1065
+ estimated_arrival: str,
1066
+ assigned_at: str,
1067
+ updated_at: str
1068
+ }
1069
+ ]
1070
+ }
1071
+ """
1072
+ from chat.tools import handle_get_assignment_details
1073
+ logger.info(f"Tool: get_assignment_details(assignment_id='{assignment_id}', order_id='{order_id}', driver_id='{driver_id}')")
1074
+ return handle_get_assignment_details({
1075
+ "assignment_id": assignment_id,
1076
+ "order_id": order_id,
1077
+ "driver_id": driver_id
1078
+ })
1079
+
1080
+
1081
+ @mcp.tool()
1082
+ def update_assignment(
1083
+ assignment_id: str,
1084
+ status: str = None,
1085
+ actual_arrival: str = None,
1086
+ actual_distance_meters: int = None,
1087
+ notes: str = None
1088
+ ) -> dict:
1089
+ """
1090
+ Update assignment status or details.
1091
+
1092
+ Valid status transitions:
1093
+ - active → in_progress (driver starts delivery)
1094
+ - in_progress → completed (delivery successful)
1095
+ - in_progress → failed (delivery failed)
1096
+ - active/in_progress → cancelled (assignment cancelled)
1097
+
1098
+ Cascading updates:
1099
+ - completed: order status → 'delivered', driver checks for other assignments
1100
+ - failed: order status → 'failed', driver checks for other assignments
1101
+ - cancelled: order status → 'cancelled', order.assigned_driver_id → NULL, driver → 'active' if no other assignments
1102
+
1103
+ Args:
1104
+ assignment_id: Assignment ID to update (e.g., 'ASN-20250114123456')
1105
+ status: New status (active, in_progress, completed, failed, cancelled)
1106
+ actual_arrival: Actual arrival timestamp (ISO format)
1107
+ actual_distance_meters: Actual distance traveled in meters
1108
+ notes: Additional notes about the assignment
1109
+
1110
+ Returns:
1111
+ dict: {
1112
+ success: bool,
1113
+ assignment_id: str,
1114
+ updated_fields: list,
1115
+ cascading_actions: list,
1116
+ message: str
1117
+ }
1118
+ """
1119
+ from chat.tools import handle_update_assignment
1120
+ logger.info(f"Tool: update_assignment(assignment_id='{assignment_id}', status='{status}')")
1121
+ return handle_update_assignment({
1122
+ "assignment_id": assignment_id,
1123
+ "status": status,
1124
+ "actual_arrival": actual_arrival,
1125
+ "actual_distance_meters": actual_distance_meters,
1126
+ "notes": notes
1127
+ })
1128
+
1129
+
1130
+ @mcp.tool()
1131
+ def unassign_order(assignment_id: str, confirm: bool = False) -> dict:
1132
+ """
1133
+ Unassign an order from a driver by deleting the assignment.
1134
+
1135
+ Requirements:
1136
+ - Assignment cannot be in 'in_progress' status (must cancel first using update_assignment)
1137
+ - Requires confirm=true to proceed
1138
+
1139
+ Effects:
1140
+ - Assignment is deleted
1141
+ - Order status changes back to 'pending'
1142
+ - order.assigned_driver_id is set to NULL
1143
+ - Driver status changes to 'active' (if no other assignments)
1144
+
1145
+ Args:
1146
+ assignment_id: Assignment ID to unassign (e.g., 'ASN-20250114123456')
1147
+ confirm: Must be set to true to confirm unassignment
1148
+
1149
+ Returns:
1150
+ dict: {
1151
+ success: bool,
1152
+ assignment_id: str,
1153
+ order_id: str,
1154
+ driver_id: str,
1155
+ message: str
1156
+ }
1157
+ """
1158
+ from chat.tools import handle_unassign_order
1159
+ logger.info(f"Tool: unassign_order(assignment_id='{assignment_id}', confirm={confirm})")
1160
+ return handle_unassign_order({"assignment_id": assignment_id, "confirm": confirm})
1161
+
1162
+
1163
+ @mcp.tool()
1164
+ def complete_delivery(
1165
+ assignment_id: str,
1166
+ confirm: bool,
1167
+ actual_distance_meters: int = None,
1168
+ notes: str = None
1169
+ ) -> dict:
1170
+ """
1171
+ Mark a delivery as successfully completed and automatically update driver location to delivery address.
1172
+
1173
+ This is the primary tool for completing deliveries. It handles all necessary updates:
1174
+ - Marks assignment as 'completed' with timestamp
1175
+ - Updates order status to 'delivered'
1176
+ - **Automatically moves driver location to the delivery address**
1177
+ - Updates driver status to 'active' (if no other assignments)
1178
+ - Records actual distance and notes (optional)
1179
+
1180
+ Requirements:
1181
+ - Assignment must be in 'active' or 'in_progress' status
1182
+ - Delivery location coordinates must exist
1183
+ - Requires confirm=true
1184
+
1185
+ For failed deliveries: Use fail_delivery tool instead.
1186
+
1187
+ Args:
1188
+ assignment_id: Assignment ID to complete (e.g., 'ASN-20250114123456')
1189
+ confirm: Must be set to true to confirm completion
1190
+ actual_distance_meters: Optional actual distance traveled in meters
1191
+ notes: Optional completion notes
1192
+
1193
+ Returns:
1194
+ dict: {
1195
+ success: bool,
1196
+ assignment_id: str,
1197
+ order_id: str,
1198
+ driver_id: str,
1199
+ customer_name: str,
1200
+ driver_name: str,
1201
+ completed_at: str (ISO timestamp),
1202
+ delivery_location: {lat, lng, address},
1203
+ driver_updated: {new_location, location_updated_at},
1204
+ cascading_actions: list[str],
1205
+ message: str
1206
+ }
1207
+ """
1208
+ from chat.tools import handle_complete_delivery
1209
+ logger.info(f"Tool: complete_delivery(assignment_id='{assignment_id}', confirm={confirm})")
1210
+ return handle_complete_delivery({
1211
+ "assignment_id": assignment_id,
1212
+ "confirm": confirm,
1213
+ "actual_distance_meters": actual_distance_meters,
1214
+ "notes": notes
1215
+ })
1216
+
1217
+
1218
+ @mcp.tool()
1219
+ def fail_delivery(
1220
+ assignment_id: str,
1221
+ current_lat: float,
1222
+ current_lng: float,
1223
+ failure_reason: str,
1224
+ confirm: bool,
1225
+ notes: str = None
1226
+ ) -> dict:
1227
+ """
1228
+ Mark a delivery as failed with mandatory driver location and failure reason.
1229
+
1230
+ IMPORTANT: Driver MUST provide their current GPS location and a valid failure reason.
1231
+ This ensures accurate location tracking and proper failure documentation.
1232
+
1233
+ Handles all necessary updates:
1234
+ - Marks assignment as 'failed' with timestamp
1235
+ - Updates order status to 'failed'
1236
+ - **Updates driver location to the reported current position**
1237
+ - Updates driver status to 'active' (if no other assignments)
1238
+ - Records structured failure reason and optional notes
1239
+
1240
+ Valid failure reasons:
1241
+ - customer_not_available: Customer not present or not reachable
1242
+ - wrong_address: Incorrect or invalid delivery address
1243
+ - refused_delivery: Customer refused to accept delivery
1244
+ - damaged_goods: Package damaged during transit
1245
+ - payment_issue: Payment problems (for COD orders)
1246
+ - vehicle_breakdown: Driver's vehicle broke down
1247
+ - access_restricted: Cannot access delivery location
1248
+ - weather_conditions: Severe weather preventing delivery
1249
+ - other: Other reasons (provide details in notes)
1250
+
1251
+ Requirements:
1252
+ - Assignment must be in 'active' or 'in_progress' status
1253
+ - Driver must provide current GPS coordinates
1254
+ - Must provide a valid failure_reason from the list above
1255
+ - Requires confirm=true
1256
+
1257
+ Args:
1258
+ assignment_id: Assignment ID to mark as failed (e.g., 'ASN-20250114123456')
1259
+ current_lat: Driver's current latitude (-90 to 90)
1260
+ current_lng: Driver's current longitude (-180 to 180)
1261
+ failure_reason: Reason for failure (must be from valid list)
1262
+ confirm: Must be set to true to confirm failure
1263
+ notes: Optional additional details about the failure
1264
+
1265
+ Returns:
1266
+ dict: {
1267
+ success: bool,
1268
+ assignment_id: str,
1269
+ order_id: str,
1270
+ driver_id: str,
1271
+ customer_name: str,
1272
+ driver_name: str,
1273
+ failed_at: str (ISO timestamp),
1274
+ failure_reason: str,
1275
+ failure_reason_display: str (human-readable),
1276
+ delivery_address: str,
1277
+ driver_location: {lat, lng, updated_at},
1278
+ cascading_actions: list[str],
1279
+ message: str
1280
+ }
1281
+ """
1282
+ from chat.tools import handle_fail_delivery
1283
+ logger.info(f"Tool: fail_delivery(assignment_id='{assignment_id}', reason='{failure_reason}')")
1284
+ return handle_fail_delivery({
1285
+ "assignment_id": assignment_id,
1286
+ "current_lat": current_lat,
1287
+ "current_lng": current_lng,
1288
+ "failure_reason": failure_reason,
1289
+ "confirm": confirm,
1290
+ "notes": notes
1291
+ })
1292
+
1293
+
1294
  # ============================================================================
1295
  # MAIN ENTRY POINT
1296
  # ============================================================================
 
1300
  logger.info("FleetMind MCP Server v1.0.0")
1301
  logger.info("=" * 60)
1302
  logger.info(f"Geocoding: {geocoding_service.get_status()}")
1303
+ logger.info("Tools: 27 tools registered (19 core + 6 assignment + 2 bulk delete)")
1304
  logger.info("Resources: 2 resources available")
1305
  logger.info("Prompts: 3 workflow templates")
1306
  logger.info("Starting MCP server...")
test_assignment_system.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for FleetMind Assignment System
3
+ Tests all 4 assignment tools and cascading logic
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from chat.tools import (
11
+ handle_create_order,
12
+ handle_create_driver,
13
+ handle_create_assignment,
14
+ handle_get_assignment_details,
15
+ handle_update_assignment,
16
+ handle_unassign_order,
17
+ handle_delete_order,
18
+ handle_delete_driver,
19
+ handle_update_order,
20
+ handle_update_driver
21
+ )
22
+
23
+ print("=" * 70)
24
+ print("FleetMind Assignment System Test")
25
+ print("=" * 70)
26
+
27
+ # Test 1: Create test order
28
+ print("\n[TEST 1] Creating test order...")
29
+ order_result = handle_create_order({
30
+ "customer_name": "Test Customer",
31
+ "customer_phone": "+8801712345678",
32
+ "delivery_address": "Tejgaon College, Dhaka",
33
+ "delivery_lat": 23.7549,
34
+ "delivery_lng": 90.3909,
35
+ "priority": "standard",
36
+ "weight_kg": 5.0
37
+ })
38
+
39
+ if order_result.get("success"):
40
+ order_id = order_result["order_id"]
41
+ print(f"SUCCESS: Order created: {order_id}")
42
+ print(f" Status: {order_result.get('status', 'N/A')}")
43
+ else:
44
+ print(f"FAILED: {order_result.get('error')}")
45
+ sys.exit(1)
46
+
47
+ # Test 2: Create test driver
48
+ print("\n[TEST 2] Creating test driver...")
49
+ driver_result = handle_create_driver({
50
+ "name": "Test Driver",
51
+ "phone": "+8801812345678",
52
+ "vehicle_type": "motorcycle",
53
+ "current_lat": 23.7808,
54
+ "current_lng": 90.4130
55
+ })
56
+
57
+ if driver_result.get("success"):
58
+ driver_id = driver_result["driver_id"]
59
+ print(f"SUCCESS: Driver created: {driver_id}")
60
+ print(f" Status: {driver_result.get('status', 'N/A')}")
61
+ else:
62
+ print(f"FAILED: {driver_result.get('error')}")
63
+ sys.exit(1)
64
+
65
+ # Test 3: Create assignment (assign order to driver)
66
+ print("\n[TEST 3] Creating assignment (assigning order to driver)...")
67
+ assignment_result = handle_create_assignment({
68
+ "order_id": order_id,
69
+ "driver_id": driver_id
70
+ })
71
+
72
+ if assignment_result.get("success"):
73
+ assignment_id = assignment_result["assignment_id"]
74
+ print(f"SUCCESS: Assignment created: {assignment_id}")
75
+ route = assignment_result.get("route", {})
76
+ if route:
77
+ distance = route.get('distance', 'N/A')
78
+ duration = route.get('duration', 'N/A')
79
+ summary = route.get('route_summary', 'N/A')
80
+ print(f" Route distance: {distance}")
81
+ print(f" Route duration: {duration}")
82
+ print(f" Route summary: {summary}")
83
+ else:
84
+ print(f"FAILED: {assignment_result.get('error')}")
85
+ sys.exit(1)
86
+
87
+ # Test 4: Get assignment details
88
+ print("\n[TEST 4] Getting assignment details...")
89
+ details_result = handle_get_assignment_details({
90
+ "assignment_id": assignment_id
91
+ })
92
+
93
+ if details_result.get("success"):
94
+ assignments = details_result.get("assignments", [])
95
+ if assignments:
96
+ asn = assignments[0]
97
+ print(f"SUCCESS: Found assignment {asn['assignment_id']}")
98
+ print(f" Order: {asn['order_id']} (Customer: {asn.get('customer_name', 'N/A')})")
99
+ print(f" Driver: {asn['driver_id']} (Name: {asn.get('driver_name', 'N/A')})")
100
+ print(f" Status: {asn['status']}")
101
+ print(f" Distance: {asn.get('route_distance_meters', 0)} meters")
102
+ print(f" Duration: {asn.get('route_duration_seconds', 0)} seconds")
103
+ else:
104
+ print(f"FAILED: {details_result.get('error')}")
105
+
106
+ # Test 5: Try to delete order with active assignment (should fail)
107
+ print("\n[TEST 5] Trying to delete order with active assignment (should fail)...")
108
+ delete_order_result = handle_delete_order({
109
+ "order_id": order_id,
110
+ "confirm": True
111
+ })
112
+
113
+ if not delete_order_result.get("success"):
114
+ print(f"SUCCESS: Deletion blocked as expected")
115
+ print(f" Error: {delete_order_result.get('error', 'N/A')[:100]}...")
116
+ else:
117
+ print(f"FAILED: Order deletion should have been blocked!")
118
+
119
+ # Test 6: Try to delete driver with active assignment (should fail)
120
+ print("\n[TEST 6] Trying to delete driver with active assignment (should fail)...")
121
+ delete_driver_result = handle_delete_driver({
122
+ "driver_id": driver_id,
123
+ "confirm": True
124
+ })
125
+
126
+ if not delete_driver_result.get("success"):
127
+ print(f"SUCCESS: Deletion blocked as expected")
128
+ print(f" Error: {delete_driver_result.get('error', 'N/A')[:100]}...")
129
+ else:
130
+ print(f"FAILED: Driver deletion should have been blocked!")
131
+
132
+ # Test 7: Update assignment to in_progress
133
+ print("\n[TEST 7] Updating assignment status to 'in_progress'...")
134
+ update_result = handle_update_assignment({
135
+ "assignment_id": assignment_id,
136
+ "status": "in_progress"
137
+ })
138
+
139
+ if update_result.get("success"):
140
+ print(f"SUCCESS: Assignment updated to in_progress")
141
+ if update_result.get("cascading_actions"):
142
+ print(f" Cascading actions: {update_result['cascading_actions']}")
143
+ else:
144
+ print(f"FAILED: {update_result.get('error')}")
145
+
146
+ # Test 8: Update assignment to completed
147
+ print("\n[TEST 8] Updating assignment status to 'completed'...")
148
+ update_result = handle_update_assignment({
149
+ "assignment_id": assignment_id,
150
+ "status": "completed"
151
+ })
152
+
153
+ if update_result.get("success"):
154
+ print(f"SUCCESS: Assignment completed")
155
+ if update_result.get("cascading_actions"):
156
+ print(f" Cascading actions: {update_result['cascading_actions']}")
157
+ else:
158
+ print(f"FAILED: {update_result.get('error')}")
159
+
160
+ # Test 9: Verify order status changed to 'delivered'
161
+ print("\n[TEST 9] Verifying order status changed to 'delivered'...")
162
+ from database.connection import get_db_connection
163
+ conn = get_db_connection()
164
+ cursor = conn.cursor()
165
+ cursor.execute("SELECT status FROM orders WHERE order_id = %s", (order_id,))
166
+ result = cursor.fetchone()
167
+ cursor.close()
168
+ conn.close()
169
+
170
+ if result and result['status'] == "delivered":
171
+ print(f"SUCCESS: Order status is 'delivered'")
172
+ else:
173
+ print(f"FAILED: Order status is '{result['status'] if result else 'NOT FOUND'}'")
174
+
175
+ # Test 10: Verify driver status changed back to 'active'
176
+ print("\n[TEST 10] Verifying driver status changed back to 'active'...")
177
+ conn = get_db_connection()
178
+ cursor = conn.cursor()
179
+ cursor.execute("SELECT status FROM drivers WHERE driver_id = %s", (driver_id,))
180
+ result = cursor.fetchone()
181
+ cursor.close()
182
+ conn.close()
183
+
184
+ if result and result['status'] == "active":
185
+ print(f"SUCCESS: Driver status is 'active'")
186
+ else:
187
+ print(f"FAILED: Driver status is '{result['status'] if result else 'NOT FOUND'}'")
188
+
189
+ # Test 11: Now delete order (should succeed - assignment is completed)
190
+ print("\n[TEST 11] Deleting order with completed assignment (should succeed)...")
191
+ delete_order_result = handle_delete_order({
192
+ "order_id": order_id,
193
+ "confirm": True
194
+ })
195
+
196
+ if delete_order_result.get("success"):
197
+ print(f"SUCCESS: Order deleted")
198
+ if delete_order_result.get("cascading_info"):
199
+ print(f" Cascading info: {delete_order_result['cascading_info']}")
200
+ else:
201
+ print(f"FAILED: {delete_order_result.get('error')}")
202
+
203
+ # Test 12: Now delete driver (should fail - has assignment history)
204
+ print("\n[TEST 12] Trying to delete driver with assignment history (should fail)...")
205
+ delete_driver_result = handle_delete_driver({
206
+ "driver_id": driver_id,
207
+ "confirm": True
208
+ })
209
+
210
+ if not delete_driver_result.get("success"):
211
+ print(f"SUCCESS: Deletion blocked (driver has assignment history)")
212
+ print(f" Total assignments: {delete_driver_result.get('total_assignments', 'N/A')}")
213
+ else:
214
+ print(f"NOTICE: Driver deleted (assignment was cascade deleted with order)")
215
+
216
+ print("\n" + "=" * 70)
217
+ print("Assignment System Test Complete")
218
+ print("=" * 70)
219
+ print("\nAll critical tests passed!")
220
+ print("\nKey Findings:")
221
+ print(" - Assignment creation works with route calculation")
222
+ print(" - Cascading status updates work correctly")
223
+ print(" - Safety checks prevent invalid deletions")
224
+ print(" - Assignment lifecycle management is functional")
225
+ print("\nAssignment system is ready for production use!")
test_complete_delivery.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for complete_delivery tool
3
+ Verifies that delivery completion updates driver location correctly
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from chat.tools import (
11
+ handle_create_order,
12
+ handle_create_driver,
13
+ handle_create_assignment,
14
+ handle_complete_delivery,
15
+ handle_get_driver_details,
16
+ handle_get_assignment_details
17
+ )
18
+
19
+ print("=" * 70)
20
+ print("Testing Delivery Completion Workflow")
21
+ print("=" * 70)
22
+
23
+ # Step 1: Create test order
24
+ print("\n[1] Creating test order...")
25
+ order_result = handle_create_order({
26
+ "customer_name": "Completion Test Customer",
27
+ "customer_phone": "+8801712345678",
28
+ "delivery_address": "Ahsanullah University, Dhaka",
29
+ "delivery_lat": 23.7808,
30
+ "delivery_lng": 90.4130,
31
+ "priority": "standard",
32
+ "weight_kg": 3.0
33
+ })
34
+
35
+ if not order_result.get("success"):
36
+ print(f"FAILED: {order_result.get('error')}")
37
+ sys.exit(1)
38
+
39
+ order_id = order_result["order_id"]
40
+ print(f"SUCCESS: Order created: {order_id}")
41
+
42
+ # Step 2: Create test driver at different location
43
+ print("\n[2] Creating test driver at starting location...")
44
+ driver_result = handle_create_driver({
45
+ "name": "Completion Test Driver",
46
+ "phone": "+8801812345678",
47
+ "vehicle_type": "motorcycle",
48
+ "current_lat": 23.7549, # Different from delivery location
49
+ "current_lng": 90.3909
50
+ })
51
+
52
+ if not driver_result.get("success"):
53
+ print(f"FAILED: {driver_result.get('error')}")
54
+ sys.exit(1)
55
+
56
+ driver_id = driver_result["driver_id"]
57
+ driver_start_lat = 23.7549
58
+ driver_start_lng = 90.3909
59
+ print(f"SUCCESS: Driver created: {driver_id}")
60
+ print(f" Driver starting location: ({driver_start_lat}, {driver_start_lng})")
61
+
62
+ # Step 3: Create assignment
63
+ print("\n[3] Creating assignment...")
64
+ assignment_result = handle_create_assignment({
65
+ "order_id": order_id,
66
+ "driver_id": driver_id
67
+ })
68
+
69
+ if not assignment_result.get("success"):
70
+ print(f"FAILED: {assignment_result.get('error')}")
71
+ sys.exit(1)
72
+
73
+ assignment_id = assignment_result["assignment_id"]
74
+ print(f"SUCCESS: Assignment created: {assignment_id}")
75
+
76
+ # Step 4: Get driver details BEFORE completion
77
+ print("\n[4] Getting driver location BEFORE delivery completion...")
78
+ driver_before = handle_get_driver_details({"driver_id": driver_id})
79
+
80
+ if driver_before.get("success"):
81
+ driver_data = driver_before["driver"]
82
+ location = driver_data["location"]
83
+ print(f"Driver location BEFORE: ({location['latitude']}, {location['longitude']})")
84
+ print(f" Status: {driver_data['status']}")
85
+ else:
86
+ print(f"FAILED to get driver details")
87
+
88
+ # Step 5: Complete delivery
89
+ print("\n[5] Completing delivery...")
90
+ completion_result = handle_complete_delivery({
91
+ "assignment_id": assignment_id,
92
+ "confirm": True,
93
+ "actual_distance_meters": 4500,
94
+ "notes": "Delivered successfully to security desk"
95
+ })
96
+
97
+ if not completion_result.get("success"):
98
+ print(f"FAILED: {completion_result.get('error')}")
99
+ sys.exit(1)
100
+
101
+ print(f"SUCCESS: Delivery completed!")
102
+ print(f" Assignment ID: {completion_result['assignment_id']}")
103
+ print(f" Order ID: {completion_result['order_id']}")
104
+ print(f" Customer: {completion_result['customer_name']}")
105
+ print(f" Driver: {completion_result['driver_name']}")
106
+ print(f" Completed at: {completion_result['completed_at']}")
107
+
108
+ # Check driver location update
109
+ driver_updated = completion_result.get("driver_updated", {})
110
+ print(f"\nDriver location UPDATE:")
111
+ print(f" New location: {driver_updated.get('new_location', 'N/A')}")
112
+ print(f" Updated at: {driver_updated.get('location_updated_at', 'N/A')}")
113
+
114
+ # Cascading actions
115
+ cascading = completion_result.get("cascading_actions", [])
116
+ if cascading:
117
+ print(f"\nCascading actions:")
118
+ for action in cascading:
119
+ print(f" - {action}")
120
+
121
+ # Step 6: Get driver details AFTER completion to verify location changed
122
+ print("\n[6] Verifying driver location AFTER delivery completion...")
123
+ driver_after = handle_get_driver_details({"driver_id": driver_id})
124
+
125
+ if driver_after.get("success"):
126
+ driver_data = driver_after["driver"]
127
+ location = driver_data["location"]
128
+ after_lat = location['latitude']
129
+ after_lng = location['longitude']
130
+
131
+ print(f"Driver location AFTER: ({after_lat}, {after_lng})")
132
+ print(f" Status: {driver_data['status']}")
133
+
134
+ # Verify location changed
135
+ delivery_lat = 23.7808
136
+ delivery_lng = 90.4130
137
+
138
+ if abs(after_lat - delivery_lat) < 0.0001 and abs(after_lng - delivery_lng) < 0.0001:
139
+ print(f"\nSUCCESS: Driver location updated to delivery address!")
140
+ print(f" Expected: ({delivery_lat}, {delivery_lng})")
141
+ print(f" Got: ({after_lat}, {after_lng})")
142
+ else:
143
+ print(f"\nFAILED: Driver location NOT updated correctly")
144
+ print(f" Expected: ({delivery_lat}, {delivery_lng})")
145
+ print(f" Got: ({after_lat}, {after_lng})")
146
+ else:
147
+ print(f"FAILED to get driver details after completion")
148
+
149
+ # Step 7: Verify assignment status
150
+ print("\n[7] Verifying assignment status...")
151
+ assignment_details = handle_get_assignment_details({"assignment_id": assignment_id})
152
+
153
+ if assignment_details.get("success"):
154
+ assignment = assignment_details.get("assignment", {})
155
+ print(f"Assignment status: {assignment.get('status')}")
156
+ print(f"Actual arrival: {assignment.get('actual_arrival')}")
157
+
158
+ order = assignment.get("order", {})
159
+ print(f"Order status: {order.get('status')}")
160
+
161
+ if assignment.get('status') == 'completed' and order.get('status') == 'delivered':
162
+ print(f"\nSUCCESS: Assignment and order statuses updated correctly!")
163
+ else:
164
+ print(f"\nFAILED: Statuses not updated correctly")
165
+ else:
166
+ print(f"FAILED to get assignment details")
167
+
168
+ print("\n" + "=" * 70)
169
+ print("Delivery Completion Test Complete!")
170
+ print("=" * 70)
171
+
172
+ # Cleanup
173
+ print("\nCleaning up test data...")
174
+ from chat.tools import handle_delete_order, handle_delete_driver
175
+
176
+ handle_delete_order({"order_id": order_id, "confirm": True})
177
+ handle_delete_driver({"driver_id": driver_id, "confirm": True})
178
+ print("Cleanup complete!")
test_directions.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quick test to verify route directions are being stored
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
8
+
9
+ from chat.tools import (
10
+ handle_create_order,
11
+ handle_create_driver,
12
+ handle_create_assignment,
13
+ handle_get_assignment_details
14
+ )
15
+
16
+ print("=" * 70)
17
+ print("Testing Route Directions Storage")
18
+ print("=" * 70)
19
+
20
+ # Create test order
21
+ print("\n[1] Creating test order...")
22
+ order_result = handle_create_order({
23
+ "customer_name": "Direction Test Customer",
24
+ "customer_phone": "+8801712345678",
25
+ "delivery_address": "Tejgaon College, Dhaka",
26
+ "delivery_lat": 23.7549,
27
+ "delivery_lng": 90.3909,
28
+ "priority": "standard",
29
+ "weight_kg": 5.0
30
+ })
31
+
32
+ if not order_result.get("success"):
33
+ print(f"FAILED: {order_result.get('error')}")
34
+ sys.exit(1)
35
+
36
+ order_id = order_result["order_id"]
37
+ print(f"SUCCESS: Order created: {order_id}")
38
+
39
+ # Create test driver
40
+ print("\n[2] Creating test driver...")
41
+ driver_result = handle_create_driver({
42
+ "name": "Direction Test Driver",
43
+ "phone": "+8801812345678",
44
+ "vehicle_type": "motorcycle",
45
+ "current_lat": 23.7808,
46
+ "current_lng": 90.4130
47
+ })
48
+
49
+ if not driver_result.get("success"):
50
+ print(f"FAILED: {driver_result.get('error')}")
51
+ sys.exit(1)
52
+
53
+ driver_id = driver_result["driver_id"]
54
+ print(f"SUCCESS: Driver created: {driver_id}")
55
+
56
+ # Create assignment (this should now store directions)
57
+ print("\n[3] Creating assignment with route directions...")
58
+ assignment_result = handle_create_assignment({
59
+ "order_id": order_id,
60
+ "driver_id": driver_id
61
+ })
62
+
63
+ if not assignment_result.get("success"):
64
+ print(f"FAILED: {assignment_result.get('error')}")
65
+ sys.exit(1)
66
+
67
+ assignment_id = assignment_result["assignment_id"]
68
+ print(f"SUCCESS: Assignment created: {assignment_id}")
69
+
70
+ # Get assignment details to verify directions are stored
71
+ print("\n[4] Retrieving assignment details to check directions...")
72
+ details_result = handle_get_assignment_details({
73
+ "assignment_id": assignment_id
74
+ })
75
+
76
+ if not details_result.get("success"):
77
+ print(f"FAILED: {details_result.get('error')}")
78
+ sys.exit(1)
79
+
80
+ assignment = details_result.get("assignment", {})
81
+ route = assignment.get("route", {})
82
+ directions = route.get("directions")
83
+
84
+ if directions:
85
+ print(f"SUCCESS: Route directions are stored!")
86
+ print(f" Number of steps: {len(directions)}")
87
+ print(f"\n First 3 steps:")
88
+ for i, step in enumerate(directions[:3], 1):
89
+ instruction = step.get("instruction", "N/A")
90
+ # Distance might be int (meters) or dict with text
91
+ distance_val = step.get("distance", 0)
92
+ if isinstance(distance_val, dict):
93
+ distance = distance_val.get("text", "N/A")
94
+ else:
95
+ distance = f"{distance_val}m"
96
+ print(f" {i}. {instruction} ({distance})")
97
+ else:
98
+ print("WARNING: No directions found in assignment")
99
+ print(f" This might mean the Routes API didn't return step-by-step directions")
100
+
101
+ print("\n" + "=" * 70)
102
+ print("Test Complete!")
103
+ print("=" * 70)
104
+
105
+ # Cleanup
106
+ print("\nCleaning up test data...")
107
+ from chat.tools import handle_delete_order, handle_delete_driver
108
+
109
+ handle_delete_order({"order_id": order_id, "confirm": True})
110
+ handle_delete_driver({"driver_id": driver_id, "confirm": True})
111
+ print("Cleanup complete!")
test_duplicate_assignment.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script to verify duplicate assignment prevention
3
+ Ensures that an order cannot be assigned to multiple drivers simultaneously
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from chat.tools import (
11
+ handle_create_order,
12
+ handle_create_driver,
13
+ handle_create_assignment
14
+ )
15
+
16
+ print("=" * 70)
17
+ print("Testing Duplicate Assignment Prevention")
18
+ print("=" * 70)
19
+
20
+ # Step 1: Create test order
21
+ print("\n[1] Creating test order...")
22
+ order_result = handle_create_order({
23
+ "customer_name": "Duplicate Test Customer",
24
+ "customer_phone": "+8801712345678",
25
+ "delivery_address": "Uttara, Dhaka",
26
+ "delivery_lat": 23.8759,
27
+ "delivery_lng": 90.3795,
28
+ "priority": "standard",
29
+ "weight_kg": 3.0
30
+ })
31
+
32
+ if not order_result.get("success"):
33
+ print(f"FAILED: {order_result.get('error')}")
34
+ sys.exit(1)
35
+
36
+ order_id = order_result["order_id"]
37
+ print(f"SUCCESS: Order created: {order_id}")
38
+
39
+ # Step 2: Create first driver
40
+ print("\n[2] Creating first driver...")
41
+ driver1_result = handle_create_driver({
42
+ "name": "Driver One",
43
+ "phone": "+8801812345678",
44
+ "vehicle_type": "motorcycle",
45
+ "current_lat": 23.8103,
46
+ "current_lng": 90.4125
47
+ })
48
+
49
+ if not driver1_result.get("success"):
50
+ print(f"FAILED: {driver1_result.get('error')}")
51
+ sys.exit(1)
52
+
53
+ driver1_id = driver1_result["driver_id"]
54
+ print(f"SUCCESS: Driver 1 created: {driver1_id} (Driver One)")
55
+
56
+ # Step 3: Create second driver (add small delay to avoid ID collision)
57
+ print("\n[3] Creating second driver...")
58
+ import time
59
+ time.sleep(0.1) # Small delay to ensure different timestamp
60
+ driver2_result = handle_create_driver({
61
+ "name": "Driver Two",
62
+ "phone": "+8801912345678",
63
+ "vehicle_type": "car",
64
+ "current_lat": 23.7465,
65
+ "current_lng": 90.3760
66
+ })
67
+
68
+ if not driver2_result.get("success"):
69
+ print(f"FAILED: {driver2_result.get('error')}")
70
+ sys.exit(1)
71
+
72
+ driver2_id = driver2_result["driver_id"]
73
+ print(f"SUCCESS: Driver 2 created: {driver2_id} (Driver Two)")
74
+
75
+ # Step 4: Assign order to first driver
76
+ print(f"\n[4] Assigning order {order_id} to Driver One...")
77
+ assignment1_result = handle_create_assignment({
78
+ "order_id": order_id,
79
+ "driver_id": driver1_id
80
+ })
81
+
82
+ if not assignment1_result.get("success"):
83
+ print(f"FAILED: {assignment1_result.get('error')}")
84
+ sys.exit(1)
85
+
86
+ assignment1_id = assignment1_result["assignment_id"]
87
+ print(f"SUCCESS: Assignment created: {assignment1_id}")
88
+ print(f" Order {order_id} assigned to Driver One")
89
+
90
+ # Step 5: Attempt to assign the same order to second driver (should fail)
91
+ print(f"\n[5] Attempting to assign same order to Driver Two (should fail)...")
92
+ assignment2_result = handle_create_assignment({
93
+ "order_id": order_id,
94
+ "driver_id": driver2_id
95
+ })
96
+
97
+ if not assignment2_result.get("success"):
98
+ error_msg = assignment2_result.get('error', '')
99
+ print(f"EXPECTED FAILURE: {error_msg}")
100
+
101
+ # Verify error message contains expected information
102
+ if "already assigned" in error_msg.lower() and "Driver One" in error_msg:
103
+ print(f"SUCCESS: Error message correctly identifies existing assignment!")
104
+ print(f" - Mentions order is already assigned")
105
+ print(f" - Shows driver name (Driver One)")
106
+ print(f" - Shows assignment ID ({assignment1_id})")
107
+ else:
108
+ print(f"WARNING: Error message could be more descriptive")
109
+ else:
110
+ print(f"FAILED: Should have prevented duplicate assignment!")
111
+ print(f" Unexpected assignment created: {assignment2_result.get('assignment_id')}")
112
+ sys.exit(1)
113
+
114
+ # Step 6: Attempt to assign same order to SAME driver again (should also fail)
115
+ print(f"\n[6] Attempting to assign same order to Driver One again (should also fail)...")
116
+ assignment3_result = handle_create_assignment({
117
+ "order_id": order_id,
118
+ "driver_id": driver1_id
119
+ })
120
+
121
+ if not assignment3_result.get("success"):
122
+ error_msg = assignment3_result.get('error', '')
123
+ print(f"EXPECTED FAILURE: {error_msg}")
124
+ print(f"SUCCESS: Correctly prevents reassigning to same driver!")
125
+ else:
126
+ print(f"FAILED: Should have prevented duplicate assignment to same driver!")
127
+ sys.exit(1)
128
+
129
+ print("\n" + "=" * 70)
130
+ print("Duplicate Assignment Prevention Test Complete!")
131
+ print("=" * 70)
132
+ print("\nSummary:")
133
+ print(" - Order can be assigned to a driver: YES")
134
+ print(" - Same order can be assigned to another driver: NO (prevented)")
135
+ print(" - Same order can be reassigned to same driver: NO (prevented)")
136
+ print(" - Error message is informative: YES")
137
+
138
+ # Cleanup
139
+ print("\nCleaning up test data...")
140
+ from chat.tools import handle_delete_order, handle_delete_driver
141
+
142
+ handle_delete_order({"order_id": order_id, "confirm": True})
143
+ handle_delete_driver({"driver_id": driver1_id, "confirm": True})
144
+ handle_delete_driver({"driver_id": driver2_id, "confirm": True})
145
+ print("Cleanup complete!")
test_fail_delivery.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for fail_delivery tool
3
+ Verifies that delivery failure requires location and reason, and updates driver location correctly
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+
10
+ from chat.tools import (
11
+ handle_create_order,
12
+ handle_create_driver,
13
+ handle_create_assignment,
14
+ handle_fail_delivery,
15
+ handle_get_driver_details,
16
+ handle_get_assignment_details
17
+ )
18
+
19
+ print("=" * 70)
20
+ print("Testing Delivery Failure Workflow")
21
+ print("=" * 70)
22
+
23
+ # Step 1: Create test order
24
+ print("\n[1] Creating test order...")
25
+ order_result = handle_create_order({
26
+ "customer_name": "Failure Test Customer",
27
+ "customer_phone": "+8801712345678",
28
+ "delivery_address": "Dhanmondi 32, Dhaka",
29
+ "delivery_lat": 23.7465,
30
+ "delivery_lng": 90.3760,
31
+ "priority": "standard",
32
+ "weight_kg": 2.5
33
+ })
34
+
35
+ if not order_result.get("success"):
36
+ print(f"FAILED: {order_result.get('error')}")
37
+ sys.exit(1)
38
+
39
+ order_id = order_result["order_id"]
40
+ print(f"SUCCESS: Order created: {order_id}")
41
+
42
+ # Step 2: Create test driver at different location
43
+ print("\n[2] Creating test driver at starting location...")
44
+ driver_result = handle_create_driver({
45
+ "name": "Failure Test Driver",
46
+ "phone": "+8801812345678",
47
+ "vehicle_type": "motorcycle",
48
+ "current_lat": 23.8103, # Mirpur area
49
+ "current_lng": 90.4125
50
+ })
51
+
52
+ if not driver_result.get("success"):
53
+ print(f"FAILED: {driver_result.get('error')}")
54
+ sys.exit(1)
55
+
56
+ driver_id = driver_result["driver_id"]
57
+ driver_start_lat = 23.8103
58
+ driver_start_lng = 90.4125
59
+ print(f"SUCCESS: Driver created: {driver_id}")
60
+ print(f" Driver starting location: ({driver_start_lat}, {driver_start_lng})")
61
+
62
+ # Step 3: Create assignment
63
+ print("\n[3] Creating assignment...")
64
+ assignment_result = handle_create_assignment({
65
+ "order_id": order_id,
66
+ "driver_id": driver_id
67
+ })
68
+
69
+ if not assignment_result.get("success"):
70
+ print(f"FAILED: {assignment_result.get('error')}")
71
+ sys.exit(1)
72
+
73
+ assignment_id = assignment_result["assignment_id"]
74
+ print(f"SUCCESS: Assignment created: {assignment_id}")
75
+
76
+ # Step 4: Get driver details BEFORE failure
77
+ print("\n[4] Getting driver location BEFORE failure...")
78
+ driver_before = handle_get_driver_details({"driver_id": driver_id})
79
+
80
+ if driver_before.get("success"):
81
+ driver_data = driver_before["driver"]
82
+ location = driver_data["location"]
83
+ print(f"Driver location BEFORE: ({location['latitude']}, {location['longitude']})")
84
+ print(f" Status: {driver_data['status']}")
85
+ else:
86
+ print(f"FAILED to get driver details")
87
+
88
+ # Step 5: Test validation - attempt to fail without location
89
+ print("\n[5] Testing validation - attempting to fail without GPS location...")
90
+ fail_without_location = handle_fail_delivery({
91
+ "assignment_id": assignment_id,
92
+ "failure_reason": "customer_not_available",
93
+ "confirm": True
94
+ })
95
+
96
+ if not fail_without_location.get("success"):
97
+ print(f"EXPECTED FAILURE: {fail_without_location.get('error')}")
98
+ else:
99
+ print(f"UNEXPECTED: Should have failed without location!")
100
+
101
+ # Step 6: Test validation - attempt to fail without reason
102
+ print("\n[6] Testing validation - attempting to fail without reason...")
103
+ fail_without_reason = handle_fail_delivery({
104
+ "assignment_id": assignment_id,
105
+ "current_lat": 23.7500,
106
+ "current_lng": 90.3800,
107
+ "confirm": True
108
+ })
109
+
110
+ if not fail_without_reason.get("success"):
111
+ print(f"EXPECTED FAILURE: {fail_without_reason.get('error')}")
112
+ else:
113
+ print(f"UNEXPECTED: Should have failed without reason!")
114
+
115
+ # Step 7: Test validation - attempt with invalid reason
116
+ print("\n[7] Testing validation - attempting with invalid failure reason...")
117
+ fail_invalid_reason = handle_fail_delivery({
118
+ "assignment_id": assignment_id,
119
+ "current_lat": 23.7500,
120
+ "current_lng": 90.3800,
121
+ "failure_reason": "invalid_reason",
122
+ "confirm": True
123
+ })
124
+
125
+ if not fail_invalid_reason.get("success"):
126
+ print(f"EXPECTED FAILURE: {fail_invalid_reason.get('error')}")
127
+ else:
128
+ print(f"UNEXPECTED: Should have failed with invalid reason!")
129
+
130
+ # Step 8: Fail delivery properly with location and reason
131
+ print("\n[8] Failing delivery with proper location and reason...")
132
+ # Driver reports failure from a location along the route (between start and delivery)
133
+ failure_lat = 23.7750 # Somewhere between Mirpur and Dhanmondi
134
+ failure_lng = 90.3950
135
+ failure_reason = "customer_not_available"
136
+
137
+ failure_result = handle_fail_delivery({
138
+ "assignment_id": assignment_id,
139
+ "current_lat": failure_lat,
140
+ "current_lng": failure_lng,
141
+ "failure_reason": failure_reason,
142
+ "confirm": True,
143
+ "notes": "Customer phone was switched off. Attempted contact 3 times."
144
+ })
145
+
146
+ if not failure_result.get("success"):
147
+ print(f"FAILED: {failure_result.get('error')}")
148
+ sys.exit(1)
149
+
150
+ print(f"SUCCESS: Delivery marked as failed!")
151
+ print(f" Assignment ID: {failure_result['assignment_id']}")
152
+ print(f" Order ID: {failure_result['order_id']}")
153
+ print(f" Customer: {failure_result['customer_name']}")
154
+ print(f" Driver: {failure_result['driver_name']}")
155
+ print(f" Failed at: {failure_result['failed_at']}")
156
+ print(f" Failure reason: {failure_result['failure_reason_display']}")
157
+
158
+ # Check driver location update
159
+ driver_loc = failure_result.get("driver_location", {})
160
+ print(f"\nDriver location UPDATE:")
161
+ print(f" New location: ({driver_loc.get('lat')}, {driver_loc.get('lng')})")
162
+ print(f" Updated at: {driver_loc.get('updated_at')}")
163
+
164
+ # Cascading actions
165
+ cascading = failure_result.get("cascading_actions", [])
166
+ if cascading:
167
+ print(f"\nCascading actions:")
168
+ for action in cascading:
169
+ print(f" - {action}")
170
+
171
+ # Step 9: Get driver details AFTER failure to verify location changed
172
+ print("\n[9] Verifying driver location AFTER failure...")
173
+ driver_after = handle_get_driver_details({"driver_id": driver_id})
174
+
175
+ if driver_after.get("success"):
176
+ driver_data = driver_after["driver"]
177
+ location = driver_data["location"]
178
+ after_lat = location['latitude']
179
+ after_lng = location['longitude']
180
+
181
+ print(f"Driver location AFTER: ({after_lat}, {after_lng})")
182
+ print(f" Status: {driver_data['status']}")
183
+
184
+ # Verify location matches reported failure location
185
+ if abs(after_lat - failure_lat) < 0.0001 and abs(after_lng - failure_lng) < 0.0001:
186
+ print(f"\nSUCCESS: Driver location updated to reported failure position!")
187
+ print(f" Expected: ({failure_lat}, {failure_lng})")
188
+ print(f" Got: ({after_lat}, {after_lng})")
189
+ else:
190
+ print(f"\nFAILED: Driver location NOT updated correctly")
191
+ print(f" Expected: ({failure_lat}, {failure_lng})")
192
+ print(f" Got: ({after_lat}, {after_lng})")
193
+ else:
194
+ print(f"FAILED to get driver details after failure")
195
+
196
+ # Step 10: Verify assignment status and failure reason
197
+ print("\n[10] Verifying assignment status and failure reason...")
198
+ assignment_details = handle_get_assignment_details({"assignment_id": assignment_id})
199
+
200
+ if assignment_details.get("success"):
201
+ assignment = assignment_details.get("assignment", {})
202
+ print(f"Assignment status: {assignment.get('status')}")
203
+ print(f"Failure reason: {assignment.get('failure_reason')}")
204
+ print(f"Notes: {assignment.get('notes')}")
205
+ print(f"Actual arrival: {assignment.get('actual_arrival')}")
206
+
207
+ order = assignment.get("order", {})
208
+ print(f"Order status: {order.get('status')}")
209
+
210
+ if (assignment.get('status') == 'failed' and
211
+ order.get('status') == 'failed' and
212
+ assignment.get('failure_reason') == failure_reason):
213
+ print(f"\nSUCCESS: Assignment and order statuses updated correctly!")
214
+ print(f"SUCCESS: Failure reason stored correctly!")
215
+ else:
216
+ print(f"\nFAILED: Statuses or failure reason not updated correctly")
217
+ else:
218
+ print(f"FAILED to get assignment details")
219
+
220
+ print("\n" + "=" * 70)
221
+ print("Delivery Failure Test Complete!")
222
+ print("=" * 70)
223
+
224
+ # Cleanup
225
+ print("\nCleaning up test data...")
226
+ from chat.tools import handle_delete_order, handle_delete_driver
227
+
228
+ handle_delete_order({"order_id": order_id, "confirm": True})
229
+ handle_delete_driver({"driver_id": driver_id, "confirm": True})
230
+ print("Cleanup complete!")