blob: 2a8589eee2bd4287a10acc9d9ac6f88fc8aefea0 [file] [log] [blame]
/*
* Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*global angular, jQuery, _*/
(function () {
'use strict';
angular.module('fraud')
.controller('FraudController', ['$scope', 'rest', 'socket', function ($scope, rest, socket) {
// topic for publishing transactions
var txTopic = 'demos.app.frauddetect.submitTransaction';
var appPromise = rest.getApp(settings.fraud.appName);
appPromise.then(function (app) {
$scope.app = app;
$scope.appURL = settings.appsURL + app.id;
$scope.startedTime = app.startedTime;
});
// Options for merchant, terminal, zip, card, bin
$scope.alertTypeTitles = {
"smallThenLarge": "Suspicious Transaction Sequence",
"sameCard": "Same Card Multiple Times",
"sameBankId": "Same Bank Number Multiple Times",
"aboveAvg": "Above Average Transaction"
}
$scope.stats = [
{ id: 'totalTxns', topic: 'demos.app.frauddetect.totalTransactions', value: 0, label: 'Total Txns' },
{ id: 'amtInLastSecond', topic: 'demos.app.frauddetect.txLastSecond', value: 0, label: 'Total Dollars / sec' },
// { id: 'amtInLastHour', topic: 'demos.app.frauddetect.txLastHour', value: 0, label: 'Total for Last Hour' },
{ id: 'avgAmtInLastSecond', topic: 'demos.app.frauddetect.avgLastSecond', value: 0, label: 'Avg Txn Amount / sec' },
{ id: 'numFrauds', topic: 'demos.app.frauddetect.totalFrauds', value: 0, label: 'No. of Anomalies Detected' },
// { id: 'avgScore', topic: 'demos.app.frauddetect.avgScore', value: 0, label: 'Average Score' }
];
$scope.merchants = ['Wal-Mart', 'Target', 'Amazon', 'Apple', 'Sears', 'Macys', 'JCPenny', 'Levis'];
$scope.terminals = [1, 2, 3, 4, 5, 6, 7, 8];
$scope.zips = [94086, 94087, 94088, 94089, 94090, 94091, 94092, 94093];
$scope.actions = [
{
id: 1,
subtitle: $scope.alertTypeTitles.smallThenLarge,
severity: 'low',
description: 'This anomaly is when one credit card is used for a small purchase, then immediately again for a larger purchase. The idea here is that a scammer will first try a small purchase to ensure that the card works, then proceed with a larger purchase upon success.',
generateTxns: function(e) {
var bin = getRandomBin();
var card = getRandomCard();
submitTransaction({
'zipCode': getRandom('zips'),
'merchantId': getRandom('merchants'),
'terminalId': getRandom('terminals'),
'bankIdNum': bin,
'ccNum': card,
'amount': 5.00
});
setTimeout(function() {
submitTransaction({
'zipCode': getRandom('zips'),
'merchantId': getRandom('merchants'),
'terminalId': getRandom('terminals'),
'bankIdNum': bin,
'ccNum': card,
'amount': 600.00
});
}, 5000)
}
},
// {
// id: 2,
// subtitle: $scope.alertTypeTitles.sameCard,
// description: 'This anomaly is when one credit card is used for multiple transactions across one or more vendors within a short time interval.',
// generateTxns: function() {
//
// var bin = getRandomBin();
// var card = getRandomCard();
// var merchant = getRandom('merchants');
//
// var intval = setInterval(function() {
// submitTransaction({
// 'zipCode': getRandom('zips'),
// 'merchantId': merchant,
// 'terminalId': getRandom('terminals'),
// 'bankIdNum': bin,
// 'ccNum': card,
// 'amount': roundToPrice(10 + Math.random() * 1000)
// });
// }, 1000);
//
// setTimeout(function() {
// clearInterval(intval);
// }, 8000);
// }
// },
{
id: 2,
subtitle: $scope.alertTypeTitles.sameBankId,
severity: 'high',
description: 'This anomaly is detected when several transactions are made by cards issued by the same bank at the same terminal ID over moving window of 30 sec.',
generateTxns: function() {
var bin = getRandomBin().replace(/\d{4}$/, '1111');
var zipCode = getRandom('zips');
var terminalId = getRandom('terminals');
var merchant = getRandom('merchants');
var intval = setInterval(function() {
submitTransaction({
'zipCode': zipCode,
'merchantId': merchant,
'terminalId': terminalId,
'bankIdNum': bin,
'ccNum': getRandomCard(),
'amount': roundToPrice(100 + Math.random() * 10)
}, true);
}, 200);
$.pnotify({
title: 'Several Transactions Being Sent',
text: '<strong>Bank ID:</strong> ' + bin + ', <strong>Merchant:</strong> ' + merchant,
type: 'success'
});
setTimeout(function() {
clearInterval(intval);
}, 11000);
}
},
{
id: 3,
subtitle: $scope.alertTypeTitles.aboveAvg,
severity: 'medium',
description: 'This anomaly is when a transaction at a given merchant significantly exceeds that merchant\'s average transaction amount.',
generateTxns: function() {
console.log('getting randomStats');
// $.get('/fraud/randomStats').done(function(res) {
// var bin = getRandomBin();
// var merchantId = res.merchantId;
// var terminalId = res.terminalId;
// var zipCode = res.zipCode;
// var amount = roundToPrice(res.sma + 35000);
//
// for (var i = 5; i >= 0; i--) {
// submitTransaction({
// 'zipCode': zipCode,
// 'merchantId': merchantId,
// 'terminalId': terminalId,
// 'bankIdNum': getRandomBin(),
// 'ccNum': getRandomCard(),
// 'amount': 20 + Math.round(Math.random() * 10)
// }, true);
// }
//
// setTimeout(function() {
// submitTransaction({
// 'zipCode': zipCode,
// 'merchantId': merchantId,
// 'terminalId': terminalId,
// 'bankIdNum': getRandomBin(),
// 'ccNum': getRandomCard(),
// 'amount': amount
// });
// }, 3000)
//
// });
$.pnotify({
'type': 'info',
'title': 'Retrieving Average Information'
});
$.get('/fraud/randomStats').done(function(res) {
var bin = getRandomBin();
var merchantId = res.merchantId;
var terminalId = res.terminalId;
var zipCode = res.zipCode;
var amount = roundToPrice(res.sma + 1500);
setTimeout(function() {
submitTransaction({
'zipCode': zipCode,
'merchantId': merchantId,
'terminalId': terminalId,
'bankIdNum': getRandomBin(),
'ccNum': getRandomCard(),
'amount': amount
});
}, 1000)
});
}
}
];
// subscribe to appropriate topics for alerts and stats
appPromise.then(function(app) {
socket.subscribe('demos.app.frauddetect.fraudAlert', function(res) {
// console.log('received fraudAlert: ', res);
// console.log(res.data.alertType, res.data.alertData ? res.data.alertData.fullCcNum : '');
if (res.data.alertType === 'aboveAvg') {
console.log('aboveAvg', res.data);
}
if (res.data.userGenerated === "true" || res.data.userGenerated === true) {
displayAlert(res.data);
}
}, $scope);
socket.subscribe('demos.app.frauddetect.txSummary', function(res) {
var data = res.data;
_.each(['amtInLastSecond','avgAmtInLastSecond','totalTxns','txnsInLastSecond'], function(key) {
// Find stat to update
var stat = _.find($scope.stats, function(obj) {
return obj.id == key;
});
// Check that stat was found
if (stat) {
if (['amtInLastSecond', 'avgAmtInLastSecond'].indexOf(key) > -1) {
stat.value = '$' + makeMoney(data[key]);
} else {
stat.value = commaGroups(data[key]);
}
}
});
$scope.$apply();
}, $scope);
});
$scope.stats.forEach(function(stat){
socket.subscribe(stat.topic, function(res) {
console.log("stat topic " + stat.topic + " data received: ", res);
stat.value = res.value;
$scope.$apply();
}, $scope);
});
$scope.alerts = [];
// helper function for choosing random items from a list
function getRandom(list) {
return $scope[list][ Math.floor(Math.random() * $scope[list].length) ];
}
function roundToPrice(amt) {
// return Math.round( amt * 100 ) / 100;
return Math.round(amt);
}
function getRandomBin() {
// Bank ID will be between 1000 0000 and 3500 0000 (25 BINs)
var base = Math.floor(Math.random() * 25) + 10;
return base + "00 0000";
}
function getRandomCard() {
// CC will be 1000 0000 to 1400 0000 (400,000 cards per BIN)
var base = Math.floor(Math.random() * 400000) + 10000000;
var baseString = base + '';
return baseString.substring(0, 4) + " " + baseString.substring(4);
}
function displayAlert(data) {
var index = $scope.alerts.push(data) - 1;
var alertTitle = $scope.alertTypeTitles[data.alertType];
var html = '';
switch(data.alertType) {
case 'smallThenLarge':
html = [
'<article class="alert-msg low" style="display:none">',
'<h1>' + alertTitle + '</h1>',
'<p>' + data.message + '</p>',
'<div><a href="#" class="btn view-txn-btn" data-txidx="' + index + '">view details</a></div>',
'</article>'
].join('');
break;
case 'sameBankId':
console.log('same bank', data);
html = [
'<article class="alert-msg high" style="display:none">',
'<h1>' + alertTitle + '</h1>',
'<p>' + data.message + '</p>',
'<div><a href="#" class="btn view-txn-btn" data-txidx="' + index + '">view details</a></div>',
'</article>'
].join('');
break;
default:
html = [
'<article class="alert-msg medium" style="display:none">',
'<h1>' + alertTitle + '</h1>',
'<p>' + data.message + '</p>',
'<div><a href="#" class="btn view-txn-btn" data-txidx="' + index + '">view details</a></div>',
'</article>'
].join('');
break;
}
var $el = $(html);
$('#alertDisplayBox').prepend($el);
$el
.slideDown()
.animate({ 'opacity': 0.5 }, 180)
.animate({ 'opacity': 1 }, 180)
.animate({ 'opacity': 0.5 }, 180)
.animate({ 'opacity': 1 }, 180)
}
function submitTransaction(txn, noMessage) {
socket.publish(txTopic, txn);
console.log('txn', txn);
if (!noMessage) {
$.pnotify({
'title': 'Transaction Submitted',
'text':
'<strong>card</strong>: ' + txn.bankIdNum + ' ' + txn.ccNum + '<br/> ' +
'<strong>amount</strong>: $' + makeMoney(txn.amount) + '<br/> ' +
'<strong>merchant</strong>: ' + txn.merchantId + ', <strong>terminal</strong>: ' + txn.terminalId,
'type': 'success'
});
}
}
function genTxnDisplayMarkup(alert) {
var html = '';
switch(alert.alertType) {
case "smallThenLarge":
var info = alert.alertData;
html = [
'<strong>Card Number:</strong> ' + info.fullCcNum + '<br/>',
'<strong>Zip Code:</strong> ' + info.zipCode + '<br/>',
'<strong>Merchant:</strong> ' + info.merchantId + ' (' + info.merchantType.toLowerCase().replace('_', ' ') + ')' + '<br/>',
'<strong>Small Amount:</strong> $' + makeMoney(info.small) + '<br/>',
'<strong>Large Amount:</strong> $' + makeMoney(info.large) + '<br/>',
'<strong>Threshold:</strong> $' + makeMoney(info.threshold) + '<br/>',
'<strong>Time:</strong> ' + new Date(1*info.time).toLocaleString() + '<br/>'
].join('');
break;
case "sameBankId":
var info = alert.alertData;
html = [
'<strong>Transaction Count:</strong> ' + info.count + '<br/>',
'<strong>Bank ID Number:</strong> ' + info.bankIdNum + '<br/>',
'<strong>Zip Code:</strong> ' + info.zipCode + '<br/>',
'<strong>Merchant:</strong> ' + info.merchantId + ' (' + info.merchantType.toLowerCase().replace('_', ' ') + ')' + '<br/>',
'<strong>Terminal:</strong> ' + info.terminalId + '<br/>',
'<strong>Time:</strong> ' + new Date(1*info.time).toLocaleString() + '<br/>'
].join('');
break;
case "aboveAvg":
var info = alert.alertData;
html = [
'<strong>Dollar Amount:</strong> $' + makeMoney(info.amount) + '<br/>',
'<strong>Last Average:</strong> $' + makeMoney(info.lastSmaValue) + '<br/>',
'<strong>Difference:</strong> $' + makeMoney(info.change) + '<br/>',
'<strong>Zip Code:</strong> ' + info.zipCode + '<br/>',
'<strong>Merchant:</strong> ' + info.merchantId + ' (' + info.merchantType.toLowerCase().replace('_', ' ') + ')' + '<br/>',
'<strong>Terminal:</strong> ' + info.terminalId + '<br/>',
'<strong>Time:</strong> ' + new Date(1*info.time).toLocaleString() + '<br/>'
].join('');
break;
}
return html;
}
function makeMoney(value) {
value = (value * 1).toFixed(2);
return commaGroups(value);
}
function commaGroups(value) {
var parts = value.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
var scopeDestroyed = false;
function updateFraudCount() {
if (scopeDestroyed) {
return;
}
if ($scope.startedTime) {
$.get('/fraud/alertCount?since=' + $scope.startedTime).done(function(res) {
var countStat = _.find($scope.stats, function(obj) {
return obj.id == 'numFrauds';
});
var value = _.reduce(res, function(memo, val, key) { return memo + val }, 0);
countStat.value = commaGroups(value);
setTimeout(updateFraudCount, 2000);
});
} else {
setTimeout(updateFraudCount, 2000);
}
}
// Set up viewing transaction modal
$('#alertDisplayBox').on('click', '.view-txn-btn', function(evt) {
evt.preventDefault();
var index = $(this).attr('data-txidx');
var alert = $scope.alerts[index];
$('#txn-modal .modal-body').html(genTxnDisplayMarkup(alert));
$('#txn-modal').modal();
});
// Start interval to poll for alerts count
updateFraudCount();
$scope.clearFrauds = function() {
$('#alertDisplayBox').html("");
}
// stop server polling on destroy (e.g. when navigating to another view)
$scope.$on('$destroy', function () {
scopeDestroyed = true;
}.bind(this));
}]);
})();