lib/states.js
const LevelsService = require('atm-state-levels');
const semver = require('semver');
const pkgjson = require('../package.json');
class StatesService{
constructor(settings, log, trace){
this.settings = settings;
this.log = log;
this.trace = trace;
this.pkg_version = pkgjson.version;
this.check_version();
if(!this.states)
this.states = {};
this.levels = new LevelsService();
}
check_version(){
if(this.settings){
let settings_version = this.settings.get('states_version');
try {
if(settings_version && !semver.lt(settings_version, this.pkg_version)){
this.states = this.restoreStates(this.settings.get('states'));
} else {
this.settings.set('states', {});
if(this.log){
this.log.error('atm_states module was updated from ' + settings_version + ' to ' + this.pkg_version + '.');
this.log.error('Please, upload ATM states from host');
}
}
} catch ( e ) {
if (e instanceof TypeError)
this.settings.set('states', {});
else
throw e;
}
this.settings.set('states_version', this.pkg_version);
}
}
restoreStates(states){
let restored = {};
for(let key in states)
restored[key] = this.convertObjectToMap(states[key]);
return restored;
}
prepareStatesToSave(){
let converted = {};
for(let key in this.states)
converted[key] = this.convertMapToObject(this.states[key]);
return converted;
}
convertMapToObject(map_state){
let object_state = {};
for(let [key, value] of map_state)
if(value instanceof Set){
// Converting Set to Array
let array = [];
for (let i of value)
array.push(i);
object_state[key] = array;
} else {
object_state[key] = value;
}
return object_state;
}
convertObjectToMap(object_state){
let map_state = new Map();
for(let key in object_state)
if(key === 'states_to'){
let states_to = new Set();
for (let i in object_state[key])
states_to.add(object_state[key][i]);
map_state.set(key, states_to);
} else {
map_state.set(key, object_state[key]);
}
return map_state;
}
/**
* [get description]
* @param {[type]} state_number [description]
* @return {[type]} [description]
*/
get(state_number){
return this.states[state_number];
}
/**
* [getEntry get the state entry, e.g. state entry 3 is a substring of original state string from position 7 to position 10 ]
* @param {[type]} data [state data to parse]
* @param {[type]} entry [state entry to get]
* @return {[type]} [3-bytes long state entry on success, null otherwise]
*/
getEntry(data, entry){
if(entry > 0 && entry < 2)
return data.substring(3, 4);
else if (entry < 10)
return data.substring(1 + 3 * (entry - 1), 4 + 3 * (entry - 1));
return null;
}
/**
* [parseState description]
* @param {[type]} data [description]
* @return {[type]} [description]
*/
parseState(data){
/**
* [addStateLinks add states_to property to the given state object. After running this function, state.states_to contains state exits]
* @param {[type]} state [state]
* @param {[type]} properties [array of properties, containing the state numbers to go, e.g. ['500', '004']]
*/
function addStateLinks(state, properties){
let states_to = new Set();
properties.forEach( (property) => {
let state_no = state.get(property);
if(state_no !== '255')
states_to.add(state_no);
});
state.set('states_to', states_to);
}
let parsed = new Map();
parsed.set('description', '');
parsed.set('number', data.substring(0, 3));
if(isNaN(parsed.get('number')))
return null;
parsed.set('type', this.getEntry(data, 1));
switch(parsed.get('type')){
case 'A':
parsed.set('description', 'Card read state');
[ 'screen_number', /* State entry 2 */
'good_read_next_state', /* State entry 3 */
'error_screen_number', /* State entry 4 */
'read_condition_1', /* State entry 5 */
'read_condition_2', /* State entry 6 */
'read_condition_3', /* State entry 7 */
'card_return_flag', /* State entry 8 */
'no_fit_match_next_state', /* State entry 9 */
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['good_read_next_state', 'no_fit_match_next_state']);
break;
case 'B':
parsed.set('description', 'PIN Entry state');
[ 'screen_number',
'timeout_next_state',
'cancel_next_state',
'local_pin_check_good_next_state',
'local_pin_check_max_bad_pins_next_state',
'local_pin_check_error_screen',
'remote_pin_check_next_state',
'local_pin_check_max_retries',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state', 'local_pin_check_good_next_state', 'local_pin_check_max_bad_pins_next_state', 'remote_pin_check_next_state']);
break;
case 'b':
parsed.set('description', 'Customer selectable PIN state');
[ 'first_entry_screen_number',
'timeout_next_state',
'cancel_next_state',
'good_read_next_state',
'csp_fail_next_state',
'second_entry_screen_number',
'mismatch_first_entry_screen_number',
'extension_state',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state', 'good_read_next_state', 'csp_fail_next_state']);
break;
case 'C':
parsed.set('description', 'Envelope Dispenser state');
['next_state',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['next_state',]);
break;
case 'D':
parsed.set('description', 'PreSet Operation Code Buffer');
[ 'next_state',
'clear_mask',
'A_preset_mask',
'B_preset_mask',
'C_preset_mask',
'D_preset_mask',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
parsed.set('extension_state', this.getEntry(data, 9));
addStateLinks(parsed, ['next_state',]);
break;
case 'E':
parsed.set('description', 'Four FDK selection state');
[ 'screen_number',
'timeout_next_state',
'cancel_next_state',
'FDK_A_next_state',
'FDK_B_next_state',
'FDK_C_next_state',
'FDK_D_next_state',
'buffer_location',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state', 'FDK_A_next_state', 'FDK_B_next_state', 'FDK_C_next_state', 'FDK_D_next_state']);
break;
case 'F':
parsed.set('description', 'Amount entry state');
[ 'screen_number',
'timeout_next_state',
'cancel_next_state',
'FDK_A_next_state',
'FDK_B_next_state',
'FDK_C_next_state',
'FDK_D_next_state',
'amount_display_screen',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state', 'FDK_A_next_state', 'FDK_B_next_state', 'FDK_C_next_state', 'FDK_D_next_state']);
break;
case 'G':
parsed.set('description', 'Amount check state');
[ 'amount_check_condition_true',
'amount_check_condition_false',
'buffer_to_check',
'integer_multiple_value',
'decimal_places',
'currency_type',
'amount_check_condition',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['amount_check_condition_true', 'amount_check_condition_false']);
break;
case 'H':
parsed.set('description', 'Information Entry State');
[ 'screen_number',
'timeout_next_state',
'cancel_next_state',
'FDK_A_next_state',
'FDK_B_next_state',
'FDK_C_next_state',
'FDK_D_next_state',
'buffer_and_display_params',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state', 'FDK_A_next_state', 'FDK_B_next_state', 'FDK_C_next_state', 'FDK_D_next_state']);
break;
case 'I':
parsed.set('description', 'Transaction request state');
[ 'screen_number',
'timeout_next_state',
'send_track2',
'send_track1_track3',
'send_operation_code',
'send_amount_data',
'send_pin_buffer',
'send_buffer_B_buffer_C',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state',]);
break;
case 'J':
parsed.set('description', 'Close state');
[ 'receipt_delivered_screen',
'next_state',
'no_receipt_delivered_screen',
'card_retained_screen_number',
'statement_delivered_screen_number',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
parsed.set('bna_notes_returned_screen', this.getEntry(data, 8));
parsed.set('extension_state', this.getEntry(data, 9));
if(parsed.get('next_state') !== '000')
addStateLinks(parsed, ['next_state']);
break;
case 'k':
parsed.set('description', 'Smart FIT check state');
parsed.set('good_read_next_state', this.getEntry(data, 3));
parsed.set('card_return_flag', this.getEntry(data, 8));
parsed.set('no_fit_match_next_state', this.getEntry(data, 9));
addStateLinks(parsed, ['good_read_next_state']);
break;
case 'K':
{
parsed.set('description', 'FIT Switch state');
let states_to = new Set();
for(let i = 2; i < 10; i += 1)
states_to.add(this.getEntry(data, i));
parsed.set('states_to', states_to);
}
break;
case 'm':
parsed.set('description', 'PIN & Language Select State');
[ 'screen_number',
'timeout_next_state',
'cancel_next_state',
'next_state_options_extension_state',
'operation_codes_extension_state',
'buffer_positions',
'FDK_active_mask',
'multi_language_screens_extension_state'
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state',]);
break;
case 'U':
parsed.set('description', 'Device Fitness Flow Select State');
[ 'device_number',
'device_available_next_state',
'device_identifier_grafic',
'device_unavailable_next_state',
'device_subcomponent_identifier'
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['device_available_next_state', 'device_unavailable_next_state',]);
break;
case 'W':
{
parsed.set('description', 'FDK Switch state');
let states = {};
let states_to = [];
['A', 'B', 'C', 'D', 'F', 'G', 'H', 'I'].forEach( (element, index) => {
states[element] = this.getEntry(data, index + 2);
states_to.push(states[element]);
});
parsed.set('states', states);
parsed.set('states_to', states_to);
}
break;
case 'X':
parsed.set('description', 'FDK information entry state');
[ 'screen_number',
'timeout_next_state',
'cancel_next_state',
'FDK_next_state',
'extension_state',
'buffer_id',
'FDK_active_mask',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state', 'FDK_next_state']);
break;
case 'Y':
parsed.set('description', 'Eight FDK selection state');
[ 'screen_number',
'timeout_next_state',
'cancel_next_state',
'FDK_next_state',
'extension_state',
'buffer_positions',
'FDK_active_mask',
'multi_language_screens',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['timeout_next_state', 'cancel_next_state', 'FDK_next_state']);
break;
case 'Z':
/*
* Accessing Z state entries may be perfromed by state.entries[i] - to get i-th table entry as it's written in NDC's spec.
* E.g. state.entries[1] is 'Z', state.entry[4] is "Z state table entry 4"
*/
{
parsed.set('description', 'Extension state');
let entries = [null, 'Z'];
for(let i = 2; i < 10; i++)
entries.push(this.getEntry(data, i));
parsed.set('entries', entries);
}
break;
case '>':
parsed.set('description', 'Cash deposit state');
[ 'cancel_key_mask',
'deposit_key_mask',
'add_more_key_mask',
'refund_key_mask',
'extension_state_1',
'extension_state_2',
'extension_state_3',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
break;
case '/':
parsed.set('description', 'Complete ICC selection');
[ 'please_wait_screen_number',
'icc_app_name_template_screen_number',
'icc_app_name_screen_number',
'extension_state',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
break;
case '?':
parsed.set('description', 'Set ICC transaction data');
[ 'next_state',
'currency_type',
'transaction_type',
'amount_authorized_source',
'amount_other_source',
'amount_too_large_next_state',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['next_state', 'amount_too_large_next_state']);
break;
case 'z':
parsed.set('description', 'EMV ICC Application Switch state');
[ 'next_state',
'terminal_aid_extension_1',
'next_state_extension_1',
'terminal_aid_extension_2',
'next_state_extension_2',
'terminal_aid_extension_3',
'next_state_extension_3',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
break;
case '+':
parsed.set('description', 'Begin ICC Initialization state');
[ 'icc_init_started_next_state',
'icc_init_not_started_next_state',
'icc_init_requirement',
'automatic_icc_app_selection_flag',
'default_app_label_usage_flag',
'cardholder_confirmation_flag',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, ['icc_init_started_next_state', 'icc_init_not_started_next_state']);
break;
case ',':
parsed.set('description', 'Complete ICC Initialization state');
[ 'please_wait_screen_number',
'icc_init_success',
'card_not_smart_next_state',
'no_usable_applications_next_state',
'icc_app_level_error_next_state',
'icc_hardware_level_error_next_state',
'no_usable_applications_fallback_next_state',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, [
'icc_init_success',
'card_not_smart_next_state',
'no_usable_applications_next_state',
'icc_app_level_error_next_state',
'icc_hardware_level_error_next_state',
'no_usable_applications_fallback_next_state',
]);
break;
case '-':
parsed.set('description', 'Automatic Language Selection state');
[ 'language_match_next_state',
'no_language_match_next_state',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, [
'language_match_next_state',
'no_language_match_next_state',
]);
break;
case '.':
parsed.set('description', 'Begin ICC Application Selection & Initialization state');
[ 'cardholder_selection_screen_number',
'FDK_template_screen_numbers_extension_state',
'action_keys_extension_state_number',
'exit_paths_extension_state_number',
'single_app_cardholder_selection_screen_number',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
break;
case ';':
parsed.set('description', 'ICC Re-initialize state');
[ 'good_read_next_state',
'processing_not_performed_next_state',
'reinit_method',
'chip_power_control',
'reset_terminal_pobjects',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed, [
'good_read_next_state',
'processing_not_performed_next_state',
]);
break;
case '&':
parsed.set('description', 'Barcode Read State');
[ 'screen_number',
'good_read_next_state',
'cancel_next_state',
'error_next_state',
'timeout_next_state',
].forEach( (element, index) => {
parsed.set(element, this.getEntry(data, index + 2));
});
addStateLinks(parsed,
[ 'good_read_next_state',
'cancel_next_state',
'error_next_state',
'timeout_next_state',
]);
break;
default:
if(this.log)
this.log.info('StatesService.parseState(): error processing state ' + parsed.number + ': unsupported state type ' + parsed.type);
return null;
}
return parsed;
}
/**
* [addStateString add state passed as a string]
* @param {[type]} state [string, e.g. '000A870500128002002002001127']
*/
addStateString(state){
let parsed = this.parseState(state);
if(parsed){
this.states[parsed.get('number')] = parsed;
if(this.log && this.trace)
this.log.info('State ' + parsed.get('number') + ' processed:' + this.trace.object(parsed));
return true;
} else
return false;
}
/**
* [addStateArray description]
* @param {[type]} state_array [state object, passed as array, e.g. ['000', 'A', '870', '500', '128', '002', '002', '002', '001', '127']]
*/
addStateArray(state_array){
let state_string = '';
let valid = true;
state_array.forEach(entry => {
if(isNaN(parseInt(entry, 10))){
// Probably it's a state type entry
if(entry.length === 0 || entry.length > 1)
valid = false;
state_string += entry;
} else {
if(entry.toString().length === 3)
state_string += entry.toString();
else if(entry.toString().length === 2)
state_string += '0' + entry.toString();
else if(entry.toString().length === 1)
state_string += '00' + entry.toString();
else if (entry.toString().length === 0)
state_string += '000';
else {
if(this.log)
this.log.error('addStateArray(): invalid state entry: ' + entry);
valid = false;
}
}
});
if(!valid)
return false;
return this.addStateString(state_string);
}
/**
* [addState description]
* @param {[type]} state [description]
* @return {boolean} [true if state was successfully added, false otherwise]
*/
addState(state){
if(typeof(state) === 'string')
return this.addStateString(state);
else if (typeof(state) === 'object')
return this.addStateArray(state);
else {
if(this.log)
this.log.error('addState() Unsupported state object type: ' + typeof(state));
return false;
}
}
/**
* [add description]
* @param {[type]} data [array of data to add]
* @return {boolean} [true if data were successfully added, false otherwise]
*/
add(data){
let result = true;
if(typeof data === 'object') {
for (let i = 0; i < data.length; i++){
if(!this.addState(data[i])){
if(this.log)
this.log.info('Error processing state ' + data[i] );
result = false;
}
}
} else if (typeof data === 'string')
result = this.addState(data);
if(result && this.settings)
this.settings.set('states', this.prepareStatesToSave());
return result;
}
/**
* [delete delete state]
* @param {[type]} state_number [number of the state to be deleted]
* @return {[type]} [true if state existed and was deleted, false if state did not exist]
*/
delete(state_number){
if(this.states[state_number]){
this.states[state_number] = undefined;
return true;
}else
return false;
}
/**
* [clearStateLevels description]
* @return {[type]} [description]
*/
clearStateLevels(){
for (let i in this.states){
let state = this.states[i];
state.set('level', null);
}
this.levels.clear();
}
/**
* [setStateLevels description]
* @param {[type]} state_numbers [description]
* @param {[type]} level [description]
*/
setStateLevels(state_numbers, level){
if(!state_numbers)
return;
state_numbers.forEach(number => {
let state = this.states[number];
if( state && !state.get('level')){
state.set('level', level);
this.levels.addState(state.get('number'), level);
let extension_state = this.getExtensionState(state);
if(extension_state){
extension_state.set('level', level);
this.levels.addState(extension_state.get('number'), level);
}
this.setStateLevels(state.get('states_to'), level + 1);
}
});
}
/**
* [updateStateLevels description]
* @return {[type]} [description]
*/
updateStateLevels(){
this.clearStateLevels();
let state = this.states['000'];
let level = 1;
state.set('level', level);
// Build a graph for states linked to state 000
this.setStateLevels(state.get('states_to'), ++level);
// Continue with the states that are not linked directly to 000
let unlinked_state_numbers = [];
for (let i in this.states){
if(this.states[i] && !this.states[i].get('level'))
unlinked_state_numbers.push(this.states[i].get('number'));
}
level = this.levels.getMaxLevel() + 3; // Add some space to separate the states
this.setStateLevels(unlinked_state_numbers, ++level);
}
/**
* [getNodes get state nodes (for state navigator)]
* @return {[type]} [array of state nodes]
*/
getNodes(){
let nodes = [];
this.updateStateLevels();
for ( let i in this.states){
let node = {};
let state = this.states[i];
if( state.get('level') !== null && state.get('type') !== 'Z'){
node.id = state.get('number');
node.label = state.get('number') + ' ' + state.get('type');
let extension_state = this.getExtensionState(state);
if(extension_state)
node.label += '\n' + extension_state.get('number') + ' ' + extension_state.get('type');
node.level = state.get('level');
nodes.push(node);
}
}
return nodes;
}
/**
* [getExtensionState description]
* @param {[type]} state [description]
* @return {[type]} [description]
*/
getExtensionState(state){
let extension_state = null;
if( state.extension_state &&
state.extension_state !== '000' &&
state.extension_state !== '255'){
extension_state = this.states[state.get('extension_state')];
if(extension_state && extension_state.get('type') === 'Z')
return extension_state;
else
return null;
}
return extension_state;
}
getEdges(){
let edges = [];
for (let i in this.states){
let state = this.states[i];
let states_to = state.get('states_to');
if(states_to){
for(let state_to of states_to){
let edge = {};
edge.from = state.get('number');
edge.to = state_to;
edges.push(edge);
}
}
/*
let states_to = state.get('states_to');
if(states_to){
states_to.forEach( state_to => {
let edge = {};
edge.from = state.get('number');
edge.to = state_to;
edges.push(edge);
});
}
*/
}
return edges;
}
}
module.exports = StatesService;