Skip to content

Commit bf0f8ed

Browse files
authored
Merge pull request #184 from devwums/fix/issue-141-velocity-fraud-rule
feat: add velocity fraud rule for geographically impossible ticket scan locations
2 parents 6ff1fb2 + 631f618 commit bf0f8ed

File tree

1 file changed

+40
-0
lines changed

1 file changed

+40
-0
lines changed

src/velocity_rule.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Fraud rule: velocity check for the same ticket scanned at geographically impossible locations.
2+
3+
A ticket is flagged when it is scanned at two *different* named locations within a
4+
configurable time window (default 30 minutes), which would require physically
5+
impossible travel between venues.
6+
"""
7+
from datetime import datetime
8+
from typing import Any, Dict, List
9+
10+
# Maximum seconds between two scans at different locations before flagging
11+
IMPOSSIBLE_TRAVEL_WINDOW_SECONDS = 1800 # 30 minutes
12+
13+
14+
def check_velocity_impossible_locations(
15+
events: List[Dict[str, Any]],
16+
window_seconds: int = IMPOSSIBLE_TRAVEL_WINDOW_SECONDS,
17+
) -> bool:
18+
"""Return True if any ticket was scanned at geographically impossible locations.
19+
20+
Groups scan events by ticket_id, sorts them by timestamp, and checks
21+
consecutive pairs: if two scans occur at different locations within
22+
*window_seconds* the rule is triggered.
23+
"""
24+
scans_by_ticket: Dict[str, List[Dict[str, Any]]] = {}
25+
for event in events:
26+
if event.get("type") == "scan":
27+
tid = str(event.get("ticket_id", ""))
28+
scans_by_ticket.setdefault(tid, []).append(event)
29+
30+
for scans in scans_by_ticket.values():
31+
scans.sort(key=lambda e: datetime.fromisoformat(str(e.get("timestamp", ""))))
32+
for i in range(len(scans) - 1):
33+
t1 = datetime.fromisoformat(str(scans[i].get("timestamp", "")))
34+
t2 = datetime.fromisoformat(str(scans[i + 1].get("timestamp", "")))
35+
loc1 = scans[i].get("location")
36+
loc2 = scans[i + 1].get("location")
37+
if loc1 and loc2 and loc1 != loc2:
38+
if (t2 - t1).total_seconds() <= window_seconds:
39+
return True
40+
return False

0 commit comments

Comments
 (0)