From b23cf543e2d8fe7c9b80a8755e065ce4d4c5628b Mon Sep 17 00:00:00 2001 From: Anthony Zhao Date: Sat, 29 Nov 2025 13:45:12 -0500 Subject: [PATCH 1/3] feat(ItemHistory location logic) --- backend/inventory/tests.py | 81 +++++++++++++++++++++++++++++++++++++- backend/inventory/utils.py | 28 ++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/backend/inventory/tests.py b/backend/inventory/tests.py index 7ce503c..0e0b16f 100644 --- a/backend/inventory/tests.py +++ b/backend/inventory/tests.py @@ -1,3 +1,80 @@ -from django.test import TestCase +import pytest +from django.utils import timezone +from datetime import timedelta +from inventory.models import CollectionItem, Location, ItemHistory +from inventory.utils import get_current_location -# Create your tests here. + +@pytest.fixture +def location_a(db): + return Location.objects.create(name="Shelf A1", location_type="STORAGE") + + +@pytest.fixture +def location_b(db): + return Location.objects.create(name="Shelf B2", location_type="STORAGE") + + +@pytest.fixture +def location_c(db): + return Location.objects.create(name="Floor - Main", location_type="FLOOR") + + +@pytest.fixture +def item(db, location_a): + return CollectionItem.objects.create(item_code="TEST001", title="Test Item", current_location=location_a) + + +@pytest.mark.django_db +class TestGetCurrentLocation: + """Tests for get_current_location algorithm validating location-changing events.""" + + @pytest.mark.parametrize("event_type", ["INITIAL", "ARRIVED", "VERIFIED", "LOCATION_CORRECTION"]) + def test_location_changing_events_update_location(self, item, location_a, location_b, event_type): + """Location-changing events should update the current location.""" + ItemHistory.objects.create(item=item, event_type="INITIAL", to_location=location_a) + ItemHistory.objects.create(item=item, event_type=event_type, to_location=location_b) + assert get_current_location(item.id) == location_b + + def test_workflow_events_ignored(self, item, location_a, location_b): + """Workflow-only events should be ignored.""" + ItemHistory.objects.create(item=item, event_type="INITIAL", to_location=location_a) + for event in ["MOVE_REQUESTED", "MOVE_APPROVED", "MOVE_REJECTED", "IN_TRANSIT"]: + ItemHistory.objects.create(item=item, event_type=event, to_location=location_b) + assert get_current_location(item.id) == location_a + + def test_most_recent_location_changing_event_wins(self, item, location_a, location_b, location_c): + """Most recent location-changing event should be returned.""" + base_time = timezone.now() + ItemHistory.objects.create(item=item, event_type="INITIAL", to_location=location_a, created_at=base_time) + ItemHistory.objects.create(item=item, event_type="ARRIVED", to_location=location_b, created_at=base_time + timedelta(hours=1)) + ItemHistory.objects.create(item=item, event_type="VERIFIED", to_location=location_c, created_at=base_time + timedelta(hours=2)) + assert get_current_location(item.id) == location_c + + def test_no_events_returns_none(self, item): + """If no location-changing events exist, return None.""" + assert get_current_location(item.id) is None + + def test_only_workflow_events_returns_none(self, item, location_a): + """If only workflow events exist, return None.""" + ItemHistory.objects.create(item=item, event_type="MOVE_REQUESTED", to_location=location_a) + assert get_current_location(item.id) is None + + def test_null_location_returns_none(self, item): + """Location-changing event with null to_location should return None.""" + ItemHistory.objects.create(item=item, event_type="INITIAL", to_location=None) + assert get_current_location(item.id) is None + + def test_invalid_item_id_returns_none(self): + """Invalid item_id should return None without raising exception.""" + assert get_current_location(99999) is None + + def test_complex_workflow_flow(self, item, location_a, location_b, location_c): + """Test realistic workflow with mixed event types.""" + base_time = timezone.now() + ItemHistory.objects.create(item=item, event_type="INITIAL", to_location=location_a, created_at=base_time) + ItemHistory.objects.create(item=item, event_type="MOVE_REQUESTED", to_location=location_b, created_at=base_time + timedelta(hours=1)) + ItemHistory.objects.create(item=item, event_type="MOVE_APPROVED", to_location=location_b, created_at=base_time + timedelta(hours=2)) + ItemHistory.objects.create(item=item, event_type="ARRIVED", to_location=location_b, created_at=base_time + timedelta(hours=3)) + ItemHistory.objects.create(item=item, event_type="VERIFIED", to_location=location_c, created_at=base_time + timedelta(hours=4)) + assert get_current_location(item.id) == location_c diff --git a/backend/inventory/utils.py b/backend/inventory/utils.py index df94e8e..6ad4e52 100644 --- a/backend/inventory/utils.py +++ b/backend/inventory/utils.py @@ -15,11 +15,37 @@ def get_current_location(item_id): - VERIFIED - LOCATION_CORRECTION + Algorithm: + Finds the most recent location-changing event (ordered by created_at descending) + and returns its to_location field. If no location-changing events exist, returns None. + Args: item_id: The ID of the CollectionItem Returns: - Location object or None + Location object or None: The Location from the most recent location-changing event's + to_location field, or None if no location-changing events exist. + + Edge Cases and Behavior: + - Multiple location-changing events: The function correctly handles multiple events + by selecting the most recent one (ordered by created_at descending). This is the + expected behavior as later events supersede earlier ones. + + - INITIAL event with to_location=None: If the INITIAL event has a null to_location, + the function will return None. This may indicate incomplete data entry. + + - Location-changing event with to_location=None: If any location-changing event + (INITIAL, ARRIVED, VERIFIED, or LOCATION_CORRECTION) has a null to_location, + the function will return None. This is unexpected for these event types and may + indicate a data integrity issue. These events should always have a to_location + set to represent the item's physical location. + + - No location-changing events: Returns None. This is expected for items that have + only workflow events (MOVE_REQUESTED, MOVE_APPROVED, etc.) but no actual location + changes yet. However, every item should ideally have at least an INITIAL event. + + - Invalid item_id: If the item_id doesn't exist, the query will return no results + and the function will return None. No exception is raised. """ # Events that actually change the physical location LOCATION_CHANGING_EVENTS = ["INITIAL", "ARRIVED", "VERIFIED", "LOCATION_CORRECTION"] From 6bd1c5d224ec4b7333edfd392a4920e7f5a79cf4 Mon Sep 17 00:00:00 2001 From: Anthony Zhao Date: Sat, 29 Nov 2025 13:56:57 -0500 Subject: [PATCH 2/3] black linting --- backend/inventory/tests.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/backend/inventory/tests.py b/backend/inventory/tests.py index 0e0b16f..ee7c37a 100644 --- a/backend/inventory/tests.py +++ b/backend/inventory/tests.py @@ -47,8 +47,12 @@ def test_most_recent_location_changing_event_wins(self, item, location_a, locati """Most recent location-changing event should be returned.""" base_time = timezone.now() ItemHistory.objects.create(item=item, event_type="INITIAL", to_location=location_a, created_at=base_time) - ItemHistory.objects.create(item=item, event_type="ARRIVED", to_location=location_b, created_at=base_time + timedelta(hours=1)) - ItemHistory.objects.create(item=item, event_type="VERIFIED", to_location=location_c, created_at=base_time + timedelta(hours=2)) + ItemHistory.objects.create( + item=item, event_type="ARRIVED", to_location=location_b, created_at=base_time + timedelta(hours=1) + ) + ItemHistory.objects.create( + item=item, event_type="VERIFIED", to_location=location_c, created_at=base_time + timedelta(hours=2) + ) assert get_current_location(item.id) == location_c def test_no_events_returns_none(self, item): @@ -73,8 +77,16 @@ def test_complex_workflow_flow(self, item, location_a, location_b, location_c): """Test realistic workflow with mixed event types.""" base_time = timezone.now() ItemHistory.objects.create(item=item, event_type="INITIAL", to_location=location_a, created_at=base_time) - ItemHistory.objects.create(item=item, event_type="MOVE_REQUESTED", to_location=location_b, created_at=base_time + timedelta(hours=1)) - ItemHistory.objects.create(item=item, event_type="MOVE_APPROVED", to_location=location_b, created_at=base_time + timedelta(hours=2)) - ItemHistory.objects.create(item=item, event_type="ARRIVED", to_location=location_b, created_at=base_time + timedelta(hours=3)) - ItemHistory.objects.create(item=item, event_type="VERIFIED", to_location=location_c, created_at=base_time + timedelta(hours=4)) + ItemHistory.objects.create( + item=item, event_type="MOVE_REQUESTED", to_location=location_b, created_at=base_time + timedelta(hours=1) + ) + ItemHistory.objects.create( + item=item, event_type="MOVE_APPROVED", to_location=location_b, created_at=base_time + timedelta(hours=2) + ) + ItemHistory.objects.create( + item=item, event_type="ARRIVED", to_location=location_b, created_at=base_time + timedelta(hours=3) + ) + ItemHistory.objects.create( + item=item, event_type="VERIFIED", to_location=location_c, created_at=base_time + timedelta(hours=4) + ) assert get_current_location(item.id) == location_c From 914a1f4a63029ddc92c0db39907e49909a5ad7be Mon Sep 17 00:00:00 2001 From: Anthony Zhao Date: Thu, 18 Dec 2025 16:13:14 -0800 Subject: [PATCH 3/3] feat(backend): Define and Document Item History & Location Logic --- backend/inventory/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/inventory/utils.py b/backend/inventory/utils.py index 6ad4e52..d276a7c 100644 --- a/backend/inventory/utils.py +++ b/backend/inventory/utils.py @@ -40,9 +40,10 @@ def get_current_location(item_id): indicate a data integrity issue. These events should always have a to_location set to represent the item's physical location. - - No location-changing events: Returns None. This is expected for items that have - only workflow events (MOVE_REQUESTED, MOVE_APPROVED, etc.) but no actual location - changes yet. However, every item should ideally have at least an INITIAL event. + - No location-changing events: Returns None only when there are no location-changing + events and no INITIAL event is present. This is expected for items that have only + workflow events (MOVE_REQUESTED, MOVE_APPROVED, etc.) but no actual location changes + yet. However, every item should ideally have at least an INITIAL event. - Invalid item_id: If the item_id doesn't exist, the query will return no results and the function will return None. No exception is raised.