Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/geofencing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ def generate_visit_hash(visit_data: dict) -> str:
# Normalize check_in_time to ISO format string for determinism
check_in_time = visit_data.get('check_in_time')
if isinstance(check_in_time, datetime):
check_in_time_str = check_in_time.isoformat()
# Normalize timestamp to UTC and remove microseconds for consistent hashing across databases
if check_in_time.tzinfo is None:
check_in_time = check_in_time.replace(tzinfo=timezone.utc)
check_in_time_str = check_in_time.astimezone(timezone.utc).replace(microsecond=0).strftime('%Y-%m-%dT%H:%M:%S')
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_visit_hash() only strips microseconds when check_in_time is a datetime. In the codebase, callers (e.g. the field officer check-in router) pass check_in_time as an ISO string (datetime.isoformat()), so this branch will still include microseconds and the hash can still change after a DB round-trip (SQLite truncation). Consider normalizing string timestamps too (e.g., parse ISO8601 -> convert/assume UTC -> drop microseconds -> re-serialize), and keep the serialized format consistent (including an explicit UTC offset/Z if you intend UTC).

Suggested change
check_in_time_str = check_in_time.astimezone(timezone.utc).replace(microsecond=0).strftime('%Y-%m-%dT%H:%M:%S')
check_in_time_str = check_in_time.astimezone(timezone.utc).replace(microsecond=0).strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(check_in_time, str):
# Parse ISO8601 string, normalize to UTC, drop microseconds, and use consistent UTC format
try:
check_in_str = check_in_time
if check_in_str.endswith('Z'):
check_in_str = check_in_str[:-1] + '+00:00'
dt = datetime.fromisoformat(check_in_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt = dt.astimezone(timezone.utc).replace(microsecond=0)
check_in_time_str = dt.strftime('%Y-%m-%dT%H:%M:%SZ')
except ValueError:
# Fall back to the original string if parsing fails
check_in_time_str = check_in_time

Copilot uses AI. Check for mistakes.
else:
check_in_time_str = str(check_in_time) if check_in_time else ""
Comment on lines 109 to 115
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether generate_visit_hash() is primarily called with string timestamps
# and confirm the current branch behavior around datetime/string normalization.

set -euo pipefail

echo "== generate_visit_hash call sites =="
rg -n -C3 --type=py '\bgenerate_visit_hash\s*\('

echo
echo "== check_in_time values built with isoformat() (string path) =="
rg -n -C3 --type=py "['\"]check_in_time['\"]\s*:\s*.*isoformat\s*\("

echo
echo "== current normalization branch in geofencing_service =="
rg -n -C4 --type=py 'if isinstance\(check_in_time, datetime\):|check_in_time_str = str\(check_in_time\) if check_in_time else ""'

Repository: RohanExploit/VishwaGuru

Length of output: 3380


Parse ISO string timestamps before hashing to guarantee deterministic behavior.

The call site at backend/routers/field_officer.py:103 passes check_in_time.isoformat() (a string), but the normalization code only handles datetime objects. String timestamps on lines 114–115 bypass UTC conversion and microsecond removal, causing hash divergence when comparing against database-altered or ISO-formatted representations.

Suggested fix
         check_in_time = visit_data.get('check_in_time')
-        if isinstance(check_in_time, datetime):
-            # Normalize timestamp to UTC and remove microseconds for consistent hashing across databases
-            if check_in_time.tzinfo is None:
-                check_in_time = check_in_time.replace(tzinfo=timezone.utc)
-            check_in_time_str = check_in_time.astimezone(timezone.utc).replace(microsecond=0).strftime('%Y-%m-%dT%H:%M:%S')
+        normalized_dt = None
+        if isinstance(check_in_time, datetime):
+            normalized_dt = check_in_time
+        elif isinstance(check_in_time, str) and check_in_time:
+            try:
+                normalized_dt = datetime.fromisoformat(check_in_time.replace("Z", "+00:00"))
+            except ValueError:
+                normalized_dt = None
+
+        if normalized_dt is not None:
+            # Normalize to UTC and remove microseconds for deterministic hashing
+            if normalized_dt.tzinfo is None:
+                normalized_dt = normalized_dt.replace(tzinfo=timezone.utc)
+            check_in_time_str = normalized_dt.astimezone(timezone.utc).replace(microsecond=0).strftime('%Y-%m-%dT%H:%M:%S')
         else:
             check_in_time_str = str(check_in_time) if check_in_time else ""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/geofencing_service.py` around lines 109 - 115, The timestamp
normalization only handles datetime objects — when check_in_time is an ISO
string it currently falls through and skips UTC normalization and microsecond
stripping, causing non-deterministic hashes; update the normalization block
around check_in_time so that if check_in_time is a string you parse it into a
datetime (e.g., with datetime.fromisoformat or a robust ISO parser), ensure
tzinfo is set/converted to timezone.utc, remove microseconds, and format to
'%Y-%m-%dT%H:%M:%S' into check_in_time_str (same target format used for datetime
inputs) so both string and datetime inputs produce identical normalized values
for hashing.


Expand Down
4 changes: 2 additions & 2 deletions backend/tests/test_detection_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async def test_detect_vandalism_with_bytes(client):
patch('backend.pothole_detection.validate_image_for_processing'), \
patch('backend.routers.detection.detect_vandalism_unified', AsyncMock(return_value=[{"label": "graffiti", "score": 0.95}])):
response = client.post(
"/detect-vandalism",
"/api/detect-vandalism",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand Down Expand Up @@ -130,7 +130,7 @@ async def test_detect_infrastructure_with_bytes(client):
patch('backend.pothole_detection.validate_image_for_processing'), \
patch('backend.routers.detection.detect_infrastructure_unified', AsyncMock(return_value=[{"label": "fallen tree", "score": 0.8}])):
response = client.post(
"/detect-infrastructure",
"/api/detect-infrastructure",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand Down
14 changes: 7 additions & 7 deletions backend/tests/test_new_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_detect_waste(client_with_mock_http):

with patch('backend.utils.validate_uploaded_file'):
response = client.post(
"/detect-waste",
"/api/detect-waste",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand Down Expand Up @@ -95,7 +95,7 @@ def test_detect_civic_eye(client_with_mock_http):

with patch('backend.utils.validate_uploaded_file'):
response = client.post(
"/detect-civic-eye",
"/api/detect-civic-eye",
files={"image": ("test.jpg", img_bytes, "image/jpeg")}
)

Expand All @@ -111,13 +111,13 @@ def test_transcribe_audio(client_with_mock_http):

audio_content = b"fake audio content"

with patch('backend.routers.detection.transcribe_audio', new_callable=AsyncMock) as mock_transcribe:
mock_transcribe.return_value = "This is a test transcription."
with patch('backend.voice_service.VoiceService.process_voice_grievance') as mock_transcribe:
mock_transcribe.return_value = {'original_text': 'This is a test transcription.', 'translated_text': 'This is a test transcription.', 'source_language': 'en', 'source_language_name': 'English', 'confidence': 0.99, 'manual_correction_needed': False, 'error': None}
response = client.post(
"/transcribe-audio",
files={"file": ("test.wav", audio_content, "audio/wav")}
"/api/voice/transcribe",
files={"audio_file": ("test.wav", audio_content, "audio/wav")}
)

assert response.status_code == 200
data = response.json()
assert data["text"] == "This is a test transcription."
assert data["original_text"] == "This is a test transcription."
2 changes: 1 addition & 1 deletion backend/tests/test_severity.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def test_detect_severity_endpoint():
with TestClient(app) as client:
# Call the endpoint
with patch('backend.utils.validate_uploaded_file'):
response = client.post("/detect-severity", files=files)
response = client.post("/api/detect-severity", files=files)

# Assertions
assert response.status_code == 200
Expand Down
30 changes: 0 additions & 30 deletions test_grievances_opt.py

This file was deleted.

Loading