Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |