UNPKG

atm-states

Version:
849 lines (746 loc) 25.9 kB
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'); // state_to is used bu State Navigator and must be Set 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); // state_exits is used by state processing, and must be an array let state_exits = []; for(let i = 2; i < 10; i += 1) state_exits.push(this.getEntry(data, i)); parsed.set('state_exits', state_exits); } 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;