diff --git a/transaction-simulator/src/main/java/com/anomaly/generator/TransactionGenerator.java b/transaction-simulator/src/main/java/com/anomaly/generator/TransactionGenerator.java index 1c05b7b4..5cdab5d7 100644 --- a/transaction-simulator/src/main/java/com/anomaly/generator/TransactionGenerator.java +++ b/transaction-simulator/src/main/java/com/anomaly/generator/TransactionGenerator.java @@ -8,63 +8,70 @@ import java.util.concurrent.ThreadLocalRandom; public class TransactionGenerator { private static final int NUM_CARDS = 10000; private static final int NUM_USERS = 5000; // Assuming each user has ~2 cards on average - private static final Map> cardLocations = new HashMap<>(); + private static final Map> cardLocations = new HashMap<>(); private static final Map cardLimits = new HashMap<>(); private static final Map cardToUser = new HashMap<>(); private static final List cardIds = new ArrayList<>(); private static final List userIds = new ArrayList<>(); - + // Anomaly types private static final int ANOMALY_NONE = 0; private static final int ANOMALY_AMOUNT = 1; private static final int ANOMALY_LOCATION = 2; private static final int ANOMALY_FREQUENCY = 3; - + // Probability of generating an anomaly (1%) private static final double ANOMALY_PROBABILITY = 0.5; - + // Initialize card and user data static { // Generate user IDs for (int i = 0; i < NUM_USERS; i++) { userIds.add("USER_" + String.format("%05d", i)); } - + // Generate card IDs and assign to users for (int i = 0; i < NUM_CARDS; i++) { String cardId = "CARD_" + String.format("%05d", i); cardIds.add(cardId); String userId = userIds.get(ThreadLocalRandom.current().nextInt(NUM_USERS)); cardToUser.put(cardId, userId); - + // Initialize empty set of locations for this card cardLocations.put(cardId, new HashSet<>()); - + // Assign random limit between $1,000 and $20,000 cardLimits.put(cardId, 1000.0 + ThreadLocalRandom.current().nextDouble() * 19000.0); } } - + public Transaction generateTransaction(boolean forceAnomaly, int anomalyType) { // Select a random card String cardId = cardIds.get(ThreadLocalRandom.current().nextInt(NUM_CARDS)); String userId = cardToUser.get(cardId); double availableLimit = cardLimits.get(cardId); - + // Determine if this should be an anomaly int actualAnomalyType = ANOMALY_NONE; if (forceAnomaly) { - actualAnomalyType = (anomalyType >= 0 && anomalyType <= 3) ? + actualAnomalyType = (anomalyType >= 0 && anomalyType <= 3) ? anomalyType : ThreadLocalRandom.current().nextInt(1, 4); } else if (ThreadLocalRandom.current().nextDouble() < ANOMALY_PROBABILITY) { - actualAnomalyType = ThreadLocalRandom.current().nextInt(1, 4); + double roll = ThreadLocalRandom.current().nextDouble(); + if (roll < 0.3) { + actualAnomalyType = ANOMALY_LOCATION; + } else if (roll < 0.8) { + actualAnomalyType = ANOMALY_AMOUNT; + } else { + actualAnomalyType = ANOMALY_FREQUENCY; + } } - + // Get or generate location - Double[] location = getLocationForCard(cardId, actualAnomalyType == ANOMALY_LOCATION); - double latitude = location[0]; - double longitude = location[1]; - + LocationPoint location = getLocationForCard(cardId, actualAnomalyType == ANOMALY_LOCATION); + double latitude = location.latitude; + double longitude = location.longitude; + // Generate transaction amount double amount; if (actualAnomalyType == ANOMALY_AMOUNT) { @@ -74,11 +81,11 @@ public class TransactionGenerator { // Normal amount (1-10% of available limit) amount = availableLimit * (0.01 + ThreadLocalRandom.current().nextDouble() * 0.09); } - + // Update available limit double newLimit = availableLimit - amount; cardLimits.put(cardId, newLimit > 0 ? newLimit : 0); - + // Create transaction Transaction transaction = new Transaction( cardId, @@ -89,56 +96,56 @@ public class TransactionGenerator { newLimit, Instant.now() ); - + return transaction; } - - private Double[] getLocationForCard(String cardId, boolean generateAnomaly) { - Set locations = cardLocations.get(cardId); - + + private LocationPoint getLocationForCard(String cardId, boolean generateAnomaly) { + Set locations = cardLocations.get(cardId); + if (locations.isEmpty() || generateAnomaly) { // Generate a random worldwide location double latitude = ThreadLocalRandom.current().nextDouble(-90, 90); double longitude = ThreadLocalRandom.current().nextDouble(-180, 180); - Double[] newLocation = {latitude, longitude}; - + LocationPoint newLocation = new LocationPoint(latitude, longitude); + // Store this location for future use unless it's an anomaly if (!generateAnomaly) { locations.add(newLocation); } - + return newLocation; } else { // Pick a random location from the card's history - Double[][] locArray = locations.toArray(new Double[0][]); + LocationPoint[] locArray = locations.toArray(new LocationPoint[0]); return locArray[ThreadLocalRandom.current().nextInt(locArray.length)]; } } - + // Method to simulate frequency anomaly by generating multiple transactions in short succession public List generateFrequencyAnomaly(String specificCardId) { List transactions = new ArrayList<>(); - String cardId = specificCardId != null ? + String cardId = specificCardId != null ? specificCardId : cardIds.get(ThreadLocalRandom.current().nextInt(NUM_CARDS)); - + // Generate 5-10 transactions in quick succession int numTransactions = ThreadLocalRandom.current().nextInt(5, 11); for (int i = 0; i < numTransactions; i++) { transactions.add(generateTransactionForCard(cardId, false, ANOMALY_NONE)); } - + return transactions; } - + private Transaction generateTransactionForCard(String cardId, boolean forceAnomaly, int anomalyType) { String userId = cardToUser.get(cardId); double availableLimit = cardLimits.get(cardId); - + // Get location - Double[] location = getLocationForCard(cardId, forceAnomaly && anomalyType == ANOMALY_LOCATION); - double latitude = location[0]; - double longitude = location[1]; - + LocationPoint location = getLocationForCard(cardId, forceAnomaly && anomalyType == ANOMALY_LOCATION); + double latitude = location.latitude; + double longitude = location.longitude; + // Generate amount double amount; if (forceAnomaly && anomalyType == ANOMALY_AMOUNT) { @@ -146,11 +153,11 @@ public class TransactionGenerator { } else { amount = availableLimit * (0.01 + ThreadLocalRandom.current().nextDouble() * 0.09); } - + // Update available limit double newLimit = availableLimit - amount; cardLimits.put(cardId, newLimit > 0 ? newLimit : 0); - + return new Transaction( cardId, userId, @@ -161,4 +168,28 @@ public class TransactionGenerator { Instant.now() ); } + + private static class LocationPoint { + final double latitude; + final double longitude; + + LocationPoint(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LocationPoint that = (LocationPoint) o; + return Double.compare(that.latitude, latitude) == 0 && + Double.compare(that.longitude, longitude) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(latitude, longitude); + } + } }