446 lines
15 KiB
JavaScript
446 lines
15 KiB
JavaScript
|
/**
|
||
|
* Support utilities for the clients
|
||
|
*/
|
||
|
'use strict';
|
||
|
const _ = require('lodash');
|
||
|
const Q = require('q');
|
||
|
const debug = require('debug')('utils:client');
|
||
|
var mainDB = require(global.pathPrefix + 'mainDB.js');
|
||
|
var utils = require(global.pathPrefix + 'utils.js');
|
||
|
var config = require(global.configFile);
|
||
|
var references = require(global.pathPrefix + '../utils/references.js');
|
||
|
var diligence = require(global.pathPrefix + '../utils/diligence/diligence.js');
|
||
|
var adminNotifier = require(global.pathPrefix + '../utils/adminNotifier.js');
|
||
|
|
||
|
const SETKYC_ERRORS = {
|
||
|
INVALID_PARAMETERS: 'BRIDGE: Invalid parameters to setKyc',
|
||
|
DOB_MISMATCH: 'BRIDGE: Date of birth doesnt match in setKyc',
|
||
|
UPDATE_FAILED: 'BRIDGE: Failed to update database in setKyc'
|
||
|
};
|
||
|
|
||
|
const SETKYC_RESPONSES = {
|
||
|
OK: 'BRIDGE: KYC complete',
|
||
|
WARNING_REFER: 'BRIDGE: Additional information required to verify identity',
|
||
|
WARNING_INTERNAL_CHECKS: 'BRIDGE: Additional internal checks required to verify identity'
|
||
|
};
|
||
|
|
||
|
module.exports = {
|
||
|
Client: Client,
|
||
|
generateEmailToken: generateEmailToken,
|
||
|
getCustomerInfo: getCustomerInfo,
|
||
|
setKyc: setKyc,
|
||
|
getDevicesInfo: getDevicesInfo,
|
||
|
|
||
|
SETKYC_ERRORS: SETKYC_ERRORS,
|
||
|
SETKYC_RESPONSES: SETKYC_RESPONSES
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Constructs a new client with appropriate default parameters.
|
||
|
* Note that this does not validate parameters - it is expected that the
|
||
|
* caller will have done all necessary validation.
|
||
|
*
|
||
|
* @class
|
||
|
* @param {String} email - email address
|
||
|
* @param {String} passwordHash - the users password, after hashing (as hex)
|
||
|
* @param {String} passwordSalt - the salt used in the hash (as hex)
|
||
|
* @param {String} operator - The account operator
|
||
|
*/
|
||
|
function Client(email, passwordHash, passwordSalt, operator) {
|
||
|
//
|
||
|
// Initialize a blank object
|
||
|
//
|
||
|
Object.assign(this, mainDB.blankClient());
|
||
|
|
||
|
//
|
||
|
// Update that object with the parameters passed in
|
||
|
//
|
||
|
this.ClientName = email;
|
||
|
this.KYC[0].ContactEmail = email;
|
||
|
this.Password = passwordHash;
|
||
|
this.ClientSalt = passwordSalt;
|
||
|
this.OperatorName = operator;
|
||
|
|
||
|
//
|
||
|
// Get the base date that expiry values are based on
|
||
|
//
|
||
|
var baseDate = new Date();
|
||
|
|
||
|
//
|
||
|
// Set up tokens for email validation
|
||
|
// This token will be valid for 7 days
|
||
|
//
|
||
|
var emailToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
|
||
|
var emailTokenExpiry = new Date(baseDate.getTime());
|
||
|
emailTokenExpiry.setDate(emailTokenExpiry.getDate() + 7);
|
||
|
|
||
|
var token = generateEmailToken();
|
||
|
this.EMailValidationToken = token.token;
|
||
|
this.EMailValidationTokenExpiry = token.expiry;
|
||
|
|
||
|
//
|
||
|
// Initialize password expiry - 1 year expiry
|
||
|
//
|
||
|
var passwordExpiry = new Date(baseDate.getTime());
|
||
|
passwordExpiry.setDate(passwordExpiry.getDate() + 365);
|
||
|
|
||
|
this.PasswordManagement[0].PasswordExpiry = passwordExpiry;
|
||
|
this.PasswordManagement[0].PasswordLastReset = baseDate;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generates an email confirmation token and expiry date
|
||
|
*
|
||
|
* @returns {Object} - Returns an object with a token and an epiry
|
||
|
*/
|
||
|
function generateEmailToken() {
|
||
|
var emailToken = utils.randomCode(utils.fullAlphaNumeric, utils.tokenLength);
|
||
|
var emailTokenExpiry = new Date();
|
||
|
emailTokenExpiry.setDate(emailTokenExpiry.getDate() + 7);
|
||
|
return {
|
||
|
token: emailToken,
|
||
|
expiry: emailTokenExpiry
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the appropriate info from the client to use as the customer in
|
||
|
* a transaction (or similar). The parmeters it returns are CustomerDisplayName,
|
||
|
* CustomerSubDisplayName, CustomerVATNo and CustomerSelfie
|
||
|
*
|
||
|
* @param {string} imageType - The type of image the user is using
|
||
|
* @param {object} client - The client object to get the info from
|
||
|
*
|
||
|
* @returns {Object | null} - the customer details (as above), or null on error
|
||
|
*/
|
||
|
function getCustomerInfo(imageType, client) {
|
||
|
//
|
||
|
// Initialise defaults for items that aren't appropriate
|
||
|
//
|
||
|
var result = {
|
||
|
CustomerDisplayName: '',
|
||
|
CustomerSubDisplayName: '',
|
||
|
CustomerImage: '',
|
||
|
CustomerVATNo: ''
|
||
|
};
|
||
|
|
||
|
//
|
||
|
// Get the correct details depending on the customer's defined image
|
||
|
//
|
||
|
switch (imageType) {
|
||
|
case 'Selfie':
|
||
|
result.CustomerDisplayName = client.DisplayName;
|
||
|
result.CustomerImage = client.Selfie;
|
||
|
break;
|
||
|
case 'defaultSelfie':
|
||
|
result.CustomerDisplayName = client.DisplayName;
|
||
|
result.CustomerImage = config.defaultSelfie;
|
||
|
break;
|
||
|
case 'CompanyLogo0':
|
||
|
result.CustomerDisplayName = client.Merchant[0].CompanyAlias;
|
||
|
result.CustomerSubDisplayName = client.Merchant[0].CompanySubName;
|
||
|
result.CustomerImage = client.Merchant[0].CompanyLogo;
|
||
|
if (client.Merchant[0].VATNo) {
|
||
|
result.CustomerVATNo = client.Merchant[0].VATNo;
|
||
|
}
|
||
|
break;
|
||
|
case 'defaultCompanyLogo0':
|
||
|
result.CustomerDisplayName = client.Merchant[0].CompanyAlias;
|
||
|
result.CustomerSubDisplayName = client.Merchant[0].CompanySubName;
|
||
|
result.CustomerImage = config.defaultCompanyLogo0;
|
||
|
if (client.Merchant[0].VATNo) {
|
||
|
result.CustomerVATNo = client.Merchant[0].VATNo;
|
||
|
}
|
||
|
break;
|
||
|
default:
|
||
|
// Something unknown so return null
|
||
|
return null;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Updates the KYC information for a client, as well as attempting automatic
|
||
|
* identitiy verification from the given information.
|
||
|
*
|
||
|
* @param {Object} client - the client object from the database to update
|
||
|
* @param {Object} updates - the new information to update from.
|
||
|
*
|
||
|
* @returns {Promise} - Promise for the success or otherwise of the update
|
||
|
* Returned values from SETKYC_RESPONSES on success,
|
||
|
* or SETKYC_ERRORS, diligence.ERRORS or references.ERRORS
|
||
|
* on error.
|
||
|
*/
|
||
|
function setKyc(client, updates) {
|
||
|
|
||
|
/**
|
||
|
* Check we've got all the required parameters
|
||
|
*/
|
||
|
if (!validateSetKycParams(client, updates)) {
|
||
|
return Q.reject(SETKYC_ERRORS.INVALID_PARAMETERS);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validate the client sent the correct DOB unless:
|
||
|
* - they haven't previously set a date of birth OR
|
||
|
* - they are currently in the REFER status of ID verification (i.e. they
|
||
|
* likely got something wrong, which could be DOB)
|
||
|
*/
|
||
|
let kyc = client.KYC[0];
|
||
|
if (
|
||
|
kyc.DateOfBirth !== '' &&
|
||
|
kyc.DateOfBirth !== updates.DateOfBirth &&
|
||
|
!utils.bitsAllSet(client.ClientStatus, utils.ClientRefer)
|
||
|
) {
|
||
|
return Q.reject(SETKYC_ERRORS.DOB_MISMATCH);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* All ok, so update with the values from the request.
|
||
|
* We do this manually rather than in a database update because we want
|
||
|
* to verifiy the details before we commit them to the DB.
|
||
|
*/
|
||
|
kyc.Title = updates.Title;
|
||
|
kyc.FirstName = updates.FirstName;
|
||
|
kyc.LastName = updates.LastName;
|
||
|
kyc.DateOfBirth = updates.DateOfBirth;
|
||
|
kyc.ResidentialAddressID = updates.ResidentialAddressID;
|
||
|
kyc.Gender = updates.Gender;
|
||
|
if (updates.hasOwnProperty('MiddleNames')) {
|
||
|
// Set the middlename. Note: convert null into ''
|
||
|
kyc.MiddleNames = updates.MiddleNames || '';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the residential address
|
||
|
*/
|
||
|
let addressP = references.isValidAddressRef(
|
||
|
client.ClientID,
|
||
|
updates.ResidentialAddressID,
|
||
|
'client.setKYC'
|
||
|
);
|
||
|
|
||
|
//
|
||
|
// Verify the person's identity with the newly updated data
|
||
|
//
|
||
|
var diligenceP = addressP.then((address) => {
|
||
|
return diligence.verifyIdentity(client, address);
|
||
|
});
|
||
|
|
||
|
//
|
||
|
// Update the record once we have verified the provided identity details
|
||
|
//
|
||
|
var updateP = diligenceP.then((diligenceResult) => {
|
||
|
//
|
||
|
// Build the query. The limits are:
|
||
|
// - Current user must be the owner (for security, to protect
|
||
|
// against Insecure Direct Object References).
|
||
|
// - DateOfBirth must match (or be unspeficied in the database, or the
|
||
|
// client's identity has not been verified)
|
||
|
//
|
||
|
var query = {
|
||
|
ClientID: client.ClientID,
|
||
|
$or: [
|
||
|
{'KYC.0.DateOfBirth': updates.DateOfBirth},
|
||
|
{'KYC.0.DateOfBirth': ''},
|
||
|
{ClientStatus: {$bitsAllSet: utils.ClientRefer}}
|
||
|
]
|
||
|
};
|
||
|
|
||
|
//
|
||
|
// Make sure the diligence result has the required defaults
|
||
|
//
|
||
|
_.defaults(
|
||
|
diligenceResult,
|
||
|
{
|
||
|
SmartScore: 998,
|
||
|
ID: '',
|
||
|
IKey: '',
|
||
|
ProfileURL: ''
|
||
|
}
|
||
|
);
|
||
|
|
||
|
//
|
||
|
// Build the update. This is slightly involved because the KYC is
|
||
|
// an array of subdocuments. We also build a new DisplayName from the
|
||
|
// FirstName + LastName.
|
||
|
//
|
||
|
var newValues = {
|
||
|
$inc: {
|
||
|
LastVersion: 1
|
||
|
},
|
||
|
$set: {
|
||
|
LastUpdate: new Date(),
|
||
|
DisplayName: updates.FirstName + ' ' + updates.LastName,
|
||
|
'KYC.0.Title': updates.Title,
|
||
|
'KYC.0.FirstName': updates.FirstName,
|
||
|
'KYC.0.LastName': updates.LastName,
|
||
|
'KYC.0.DateOfBirth': updates.DateOfBirth,
|
||
|
'KYC.0.ResidentialAddressID': updates.ResidentialAddressID,
|
||
|
'KYC.0.Gender': updates.Gender,
|
||
|
'KYC.0.Smartscore': diligenceResult.Smartscore,
|
||
|
'KYC.0.ID': diligenceResult.ID,
|
||
|
'KYC.0.IKey': diligenceResult.IKey,
|
||
|
'KYC.0.ProfileURL': diligenceResult.ProfileURL
|
||
|
}
|
||
|
};
|
||
|
if (updates.hasOwnProperty('MiddleNames')) {
|
||
|
// Set the middlename. Note: convert null into ''
|
||
|
newValues.$set['KYC.0.MiddleNames'] = updates.MiddleNames || '';
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Work out the status bits we need to update in the client
|
||
|
//
|
||
|
let status = utils.ClientDetailsMask;
|
||
|
let response = SETKYC_RESPONSES.OK;
|
||
|
if (_.isArray(diligenceResult.Warnings)) {
|
||
|
for (let i = 0; i < diligenceResult.Warnings.length; ++i) {
|
||
|
// Don't report errors with bitwise operations
|
||
|
// jshint -W016
|
||
|
switch (diligenceResult.Warnings[i]) {
|
||
|
case diligence.WARNINGS.REFER:
|
||
|
status |= utils.ClientRefer;
|
||
|
response = SETKYC_RESPONSES.WARNING_REFER;
|
||
|
break;
|
||
|
|
||
|
case diligence.WARNINGS.PEPS:
|
||
|
status |= utils.ClientPeps;
|
||
|
break;
|
||
|
|
||
|
case diligence.WARNINGS.SANCTIONS:
|
||
|
status |= utils.ClientSanctions;
|
||
|
response = SETKYC_RESPONSES.WARNING_INTERNAL_CHECKS;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
newValues.$bit = {
|
||
|
ClientStatus: {
|
||
|
or: status
|
||
|
}
|
||
|
};
|
||
|
|
||
|
//
|
||
|
// Build the options
|
||
|
//
|
||
|
var options = {
|
||
|
returnOriginal: false, // Need the updated document
|
||
|
upsert: false // Don't upsert if not found
|
||
|
};
|
||
|
|
||
|
//
|
||
|
// Make the request
|
||
|
//
|
||
|
return Q.ninvoke(
|
||
|
mainDB.collectionClient,
|
||
|
'findOneAndUpdate',
|
||
|
query,
|
||
|
newValues,
|
||
|
options
|
||
|
).then((result) => {
|
||
|
if (!result.ok || !result.value) {
|
||
|
//
|
||
|
// Nothing found - most likely some mistmatch in the search
|
||
|
//
|
||
|
return Q.reject(SETKYC_ERRORS.UPDATE_FAILED);
|
||
|
} else {
|
||
|
//
|
||
|
// Success (or success with warning).
|
||
|
// If the status is not a clean pass, then notify the admin
|
||
|
//
|
||
|
if (status !== utils.ClientDetailsMask) {
|
||
|
adminNotifier.notifyIdentityCheckIssue(result.value);
|
||
|
}
|
||
|
return Q.resolve(response);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Return the result of the final promise, assuming they all pass
|
||
|
*/
|
||
|
return Q.all([addressP, diligenceP, updateP]).then((responses) => responses[2]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates that the parameters passed in are sufficient to update the KYC
|
||
|
*
|
||
|
* @param {Object} client - the client object to be updated
|
||
|
* @param {Object} updates - the update values
|
||
|
* @return {boolean} - true if the values are valid, false otherwise
|
||
|
*/
|
||
|
function validateSetKycParams(client, updates) {
|
||
|
const required = [
|
||
|
'Title', 'FirstName', 'LastName', 'DateOfBirth', 'Gender', 'ResidentialAddressID'
|
||
|
];
|
||
|
for (let i = 0; i < required.length; ++i) {
|
||
|
if (!_.isString(updates[required[i]])) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!_.isObject(client) || !_.isArray(client.KYC) || client.KYC.length <= 0) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets information about the devices a client has. This function returns a
|
||
|
* promise for an object with two values:
|
||
|
* - hasDevices: does the client have any devices (in any state)
|
||
|
* - hasActiveDevice: does the client have at least one device that is fully active
|
||
|
* i.e. fully registered, not disabled, not barred, etc
|
||
|
*
|
||
|
* @param {string} clientID - the client ID we are interested in
|
||
|
* @returns {Promise<object>} - promise for the status info
|
||
|
*/
|
||
|
function getDevicesInfo(clientID) {
|
||
|
const query = {
|
||
|
ClientID: clientID
|
||
|
};
|
||
|
const projection = {
|
||
|
_id: 0,
|
||
|
DeviceStatus: 1 // Only need device status
|
||
|
};
|
||
|
|
||
|
let result = {
|
||
|
hasDevices: false,
|
||
|
hasActiveDevice: false
|
||
|
};
|
||
|
|
||
|
debug('Getting device info');
|
||
|
|
||
|
//
|
||
|
// Create an async/generator function to simplify looping over the results of find.
|
||
|
// This also lets us end early, and not force loading everything into an array
|
||
|
//
|
||
|
return Q.async(function*() {
|
||
|
let cursor = mainDB.collectionDevice.find(query, projection);
|
||
|
|
||
|
while (yield cursor.hasNext()) {
|
||
|
debug(' -- hasNext');
|
||
|
result.hasDevices = true; // We have at least one device
|
||
|
|
||
|
let device = yield cursor.next();
|
||
|
debug(' -- next:', device);
|
||
|
let status = device.DeviceStatus;
|
||
|
if (
|
||
|
utils.bitsAllSet(status, utils.DeviceFullyRegistered) &&
|
||
|
!utils.bitsAllSet(status, utils.DeviceSuspendedMask) &&
|
||
|
!utils.bitsAllSet(status, utils.DeviceBarredMask)
|
||
|
) {
|
||
|
result.hasActiveDevice = true;
|
||
|
|
||
|
debug(' -- found active device:');
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
debug(' - returning result');
|
||
|
return result;
|
||
|
})();
|
||
|
}
|