swiftops-backend / tests /unit /test_ticket_location_service.py
kamau1's picture
feat: auto-populate and verify ticket work locations from assignment arrival coordinates
f8f7bb6
"""
Unit tests for TicketLocationService
Tests work location derivation and verification logic.
"""
import pytest
from decimal import Decimal
from unittest.mock import Mock
from app.services.ticket_location_service import TicketLocationService
class TestHaversineDistance:
"""Test distance calculation"""
def test_same_location(self):
"""Distance between same coordinates should be 0"""
distance = TicketLocationService.haversine_distance(
-1.1005, 37.0092,
-1.1005, 37.0092
)
assert distance < 1 # Less than 1 meter
def test_known_distance(self):
"""Test with known coordinates (approximately 1km apart)"""
# Nairobi CBD coordinates roughly 1km apart
distance = TicketLocationService.haversine_distance(
-1.2864, 36.8172, # Point A
-1.2921, 36.8219 # Point B
)
# Should be around 700-800 meters
assert 600 < distance < 900
def test_close_proximity(self):
"""Test coordinates within 100m (same building)"""
distance = TicketLocationService.haversine_distance(
-1.10052333, 37.00922667, # Journey start
-1.10056144, 37.00923637 # Arrival
)
# Should be less than 100m
assert distance < 100
class TestDeriveWorkLocation:
"""Test work location derivation from assignment"""
def test_derive_from_arrival_success(self):
"""Should derive work location when ticket has none"""
# Mock ticket with no work location
ticket = Mock()
ticket.work_location_latitude = None
ticket.work_location_longitude = None
# Mock assignment with arrival coordinates
assignment = Mock()
assignment.id = "test-assignment-id"
assignment.arrival_latitude = Decimal("-1.10056144")
assignment.arrival_longitude = Decimal("37.00923637")
# Derive location
updated, message = TicketLocationService.derive_work_location_from_assignment(
ticket, assignment
)
assert updated is True
assert "derived" in message.lower()
assert ticket.work_location_latitude == Decimal("-1.10056144")
assert ticket.work_location_longitude == Decimal("37.00923637")
assert ticket.work_location_verified is True
def test_derive_when_location_exists(self):
"""Should not derive when ticket already has location"""
# Mock ticket with existing work location
ticket = Mock()
ticket.work_location_latitude = Decimal("-1.2201")
ticket.work_location_longitude = Decimal("36.8775")
# Mock assignment with arrival coordinates
assignment = Mock()
assignment.arrival_latitude = Decimal("-1.10056144")
assignment.arrival_longitude = Decimal("37.00923637")
# Try to derive location
updated, message = TicketLocationService.derive_work_location_from_assignment(
ticket, assignment
)
assert updated is False
assert "already has" in message.lower()
def test_derive_without_arrival_coordinates(self):
"""Should not derive when assignment has no arrival coordinates"""
# Mock ticket with no work location
ticket = Mock()
ticket.work_location_latitude = None
ticket.work_location_longitude = None
# Mock assignment without arrival coordinates
assignment = Mock()
assignment.arrival_latitude = None
assignment.arrival_longitude = None
# Try to derive location
updated, message = TicketLocationService.derive_work_location_from_assignment(
ticket, assignment
)
assert updated is False
assert "no arrival" in message.lower()
class TestVerifyWorkLocation:
"""Test work location verification against arrival"""
def test_verify_within_threshold(self):
"""Should verify when coordinates are within 100m"""
# Mock ticket with work location
ticket = Mock()
ticket.id = "test-ticket-id"
ticket.work_location_latitude = Decimal("-1.10052333")
ticket.work_location_longitude = Decimal("37.00922667")
# Mock assignment with nearby arrival (within 100m)
assignment = Mock()
assignment.arrival_latitude = Decimal("-1.10056144")
assignment.arrival_longitude = Decimal("37.00923637")
# Verify location
verified, message, distance = TicketLocationService.verify_work_location_against_arrival(
ticket, assignment
)
assert verified is True
assert "verified" in message.lower()
assert distance < 100
assert ticket.work_location_verified is True
def test_verify_exceeds_threshold(self):
"""Should not verify when coordinates are beyond 100m"""
# Mock ticket with work location
ticket = Mock()
ticket.id = "test-ticket-id"
ticket.work_location_latitude = Decimal("-1.2201")
ticket.work_location_longitude = Decimal("36.8775")
# Mock assignment with far arrival (> 100m)
assignment = Mock()
assignment.arrival_latitude = Decimal("-1.10056144")
assignment.arrival_longitude = Decimal("37.00923637")
# Verify location
verified, message, distance = TicketLocationService.verify_work_location_against_arrival(
ticket, assignment
)
assert verified is False
assert "too far" in message.lower()
assert distance > 100
def test_verify_without_work_location(self):
"""Should not verify when ticket has no work location"""
# Mock ticket without work location
ticket = Mock()
ticket.work_location_latitude = None
ticket.work_location_longitude = None
# Mock assignment with arrival
assignment = Mock()
assignment.arrival_latitude = Decimal("-1.10056144")
assignment.arrival_longitude = Decimal("37.00923637")
# Try to verify
verified, message, distance = TicketLocationService.verify_work_location_against_arrival(
ticket, assignment
)
assert verified is False
assert "no work location" in message.lower()
assert distance is None
class TestUpdateWorkLocationOnCompletion:
"""Test complete workflow on ticket completion"""
def test_derive_when_missing(self):
"""Should derive location when ticket has none"""
# Mock ticket without work location
ticket = Mock()
ticket.work_location_latitude = None
ticket.work_location_longitude = None
# Mock assignment with arrival
assignment = Mock()
assignment.id = "test-assignment-id"
assignment.arrival_latitude = Decimal("-1.10056144")
assignment.arrival_longitude = Decimal("37.00923637")
# Update on completion
result = TicketLocationService.update_work_location_on_completion(
ticket, assignment
)
assert result["action"] == "derived"
assert result["success"] is True
assert ticket.work_location_latitude is not None
def test_verify_when_exists(self):
"""Should verify location when ticket has one"""
# Mock ticket with work location
ticket = Mock()
ticket.id = "test-ticket-id"
ticket.work_location_latitude = Decimal("-1.10052333")
ticket.work_location_longitude = Decimal("37.00922667")
# Mock assignment with nearby arrival
assignment = Mock()
assignment.arrival_latitude = Decimal("-1.10056144")
assignment.arrival_longitude = Decimal("37.00923637")
# Update on completion
result = TicketLocationService.update_work_location_on_completion(
ticket, assignment
)
assert result["action"] == "verified"
assert result["success"] is True
assert result["distance_meters"] < 100