timgabets/electron-atm

View on GitHub
src/controllers/atm.js

Summary

Maintainability
F
1 wk
Test Coverage
const StatesService = require('atm-states');
const ScreensService = require('atm-screens');
const FITsService = require('atm-fits');
const CryptoService = require('../services/crypto.js');
const DisplayService = require('../services/display.js');
const OperationCodeBufferService = require('atm-opcode-buffer');
const Trace = require('atm-trace');
const Pinblock = require('pinblock');
const des3 = require('node-cardcrypto').des;
const ATMHardwareService = require('atm-hardware');

class ATM {
  constructor(settings, log){
    this.settings = settings;
    this.log = log;
    this.trace = new Trace();
    this.states = new StatesService(settings, log, this.trace);
    this.screens = new ScreensService(settings, log, this.trace);
    this.FITs = new FITsService(settings, log, this.trace);
    this.crypto = new CryptoService(settings, log);
    this.display = new DisplayService(this.screens, log);
    this.pinblock = new Pinblock();
    this.opcode = new OperationCodeBufferService();
    this.hardware = new ATMHardwareService();

    this.setStatus('Offline');
    this.initBuffers();
    this.initCounters();
    this.current_state = null;
    this.buttons_pressed = [];
    this.activeFDKs = [];
    this.transaction_request = null;
  }

  getBuffer(type){
    switch(type){
    case 'pin':
      return this.PIN_buffer;
    case 'B':
      return this.buffer_B;
    case 'C':
      return this.buffer_C;
    case 'opcode':
      return this.opcode.getBuffer();
    case 'amount':
      return this.amount_buffer;
    }
  }

  /**
   * [isFDKButtonActive check whether the FDKs is active or not]
   * @param  {[type]}  button [FDK button to be checked, e.g. 'A', 'G' (case does not matter - 'a', 'g' works as well) ]
   * @return {Boolean}        [true if FDK is active, false if inactive]
   */
  isFDKButtonActive(button){
    if(!button)
      return;

    for (let i = 0; i < this.activeFDKs.length; i++)
      if(button.toUpperCase() === this.activeFDKs[i] )
        return true; 
    
    return false;
  }

  /**
   * [setFDKsActiveMask set the current FDK mask ]
   * @param {[type]} mask [1. number from 000 to 255, represented as string, OR
   *                       2. binary mask, represented as string, e.g. 100011000 ]
   */
  setFDKsActiveMask(mask){
    if(mask.length <= 3 && mask.length !== 0){
      // 1. mask is a number from 000 to 255, represented as string
      if(mask > 255){
        this.log.error('Invalid FDK mask: ' + mask);
        return;
      }

      this.activeFDKs = [];
      let FDKs = ['A', 'B', 'C', 'D', 'F', 'G', 'H', 'I'];  // E excluded
      for(let bit = 0; bit < 8; bit++)
        if((mask & Math.pow(2, bit)).toString() !== '0')
          this.activeFDKs.push(FDKs[bit]);

    } else if(mask.length > 0) {
      // 2. mask is a binary mask, represented as string, e.g. 100011000 
      this.activeFDKs = [];
      
      // The first character of the mask is a 'Numeric Keys activator', and is not currently processed
      mask = mask.substr(1, mask.length);

      let FDKs = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']; // E included
      for(let i = 0; i < mask.length; i++)
        if(mask[i] === '1')
          this.activeFDKs.push(FDKs[i]);
    } else  
      this.log.error('Empty FDK mask');
  }

  getTerminalStateReply(command_code){
    let reply = {};

    if(command_code)
      reply.terminal_command = command_code;

    switch(command_code){
    case 'Send Configuration Information':
      reply.config_id = this.getConfigID();
      reply.hardware_fitness = this.hardware.getHardwareFitness();
      reply.hardware_configuration = '157F000901020483000001B1000000010202047F7F00';
      reply.supplies_status = this.hardware.getSuppliesStatus();
      reply.sensor_status = '000000000000';
      reply.release_number = this.hardware.getReleaseNumber();
      reply.ndc_software_id = this.hardware.getHarwareID();
      break;

    case 'Send Configuration ID':
      reply.config_id = this.getConfigID();
      break;

    case 'Send Supply Counters':
      {
        let counters = this.getSupplyCounters();
        for(let c in counters) reply[c] = counters[c];
      }
      break;

    default:
      break;
    }

    return reply;
  }

  /**
   * [replySolicitedStatus description]
   * @param  {[type]} status [description]
   * @return {[type]}        [description]
   */
  replySolicitedStatus(status, command_code){
    let reply = {};
    reply.message_class = 'Solicited';
    reply.message_subclass = 'Status'; 

    switch(status){
    case 'Ready':
    case 'Command Reject':
    case 'Specific Command Reject':
      reply.status_descriptor = status;
      break;
        
    case 'Terminal State':
      {
        reply.status_descriptor = status;
        let data = this.getTerminalStateReply(command_code);

        for(let c in data) reply[c] = data[c];
      }
      break;

    default:
      this.log.error('atm.replySolicitedStatus(): unknown status ' + status);
      reply.status_descriptor = 'Command Reject';
    }
    return reply;
  }

  /**
   * [processTerminalCommand description]
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  processTerminalCommand(data){
    switch(data.command_code){
    case 'Go in-service':
      this.setStatus('In-Service');
      this.processState('000');
      this.initBuffers();
      this.activeFDKs = [];
      break;
    case 'Go out-of-service':
      this.setStatus('Out-Of-Service');
      this.initBuffers();
      this.activeFDKs = [];
      this.card = null;
      break;
    case 'Send Configuration Information':
    case 'Send Configuration ID':
    case 'Send Supply Counters':
      return this.replySolicitedStatus('Terminal State', data.command_code);

    default:
      this.log.error('atm.processTerminalCommand(): unknown command code: ' + data.command_code);
      return this.replySolicitedStatus('Command Reject');
    }
    return this.replySolicitedStatus('Ready');
  }

  /**
   * [processCustomizationCommand description]
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  processCustomizationCommand(data){
    switch(data.message_identifier){
    case 'Screen Data load':
      if(this.screens.add(data.screens))
        return this.replySolicitedStatus('Ready'); 
      else
        return this.replySolicitedStatus('Command Reject');

    case 'State Tables load':
      if(this.states.add(data.states))
        return this.replySolicitedStatus('Ready'); 
      else
        return this.replySolicitedStatus('Command Reject');

    case 'FIT Data load':
      if(this.FITs.add(data.FITs))
        return this.replySolicitedStatus('Ready');
      else
        return this.replySolicitedStatus('Command Reject');

    case 'Configuration ID number load':
      if(data.config_id){
        this.setConfigID(data.config_id);
        return this.replySolicitedStatus('Ready');
      }else{
        this.log.info('ATM.processDataCommand(): no Config ID provided');
        return this.replySolicitedStatus('Command Reject');
      }

    default:
      this.log.error('ATM.processDataCommand(): unknown message identifier: ', data.message_identifier);
      return this.replySolicitedStatus('Command Reject');
    }
  }

  /**
   * [processInteractiveTransactionResponse description]
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  processInteractiveTransactionResponse(data){
    this.interactive_transaction = true;

    if(data.active_keys){
      this.setFDKsActiveMask(data.active_keys);
    }
    
    this.display.setScreen(this.screens.parseDynamicScreenData(data.screen_data_field));
    return this.replySolicitedStatus('Ready');
  }

  processExtendedEncKeyInfo(data){
    switch(data.modifier){
    case 'Decipher new comms key with current master key':
      if( this.crypto.setCommsKey(data.new_key_data, data.new_key_length) )
        return this.replySolicitedStatus('Ready');
      else
        return this.replySolicitedStatus('Command Reject');

    default:
      this.log.error('Unsupported modifier');
      break;
    }

    return this.replySolicitedStatus('Command Reject');
  }

  /**
   * [processDataCommand description]
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  processDataCommand(data){
    switch(data.message_subclass){
    case 'Customization Command':
      return this.processCustomizationCommand(data);

    case 'Interactive Transaction Response':
      return this.processInteractiveTransactionResponse(data);

    case 'Extended Encryption Key Information':
      return this.processExtendedEncKeyInfo(data);
        
    default:
      this.log.info('atm.processDataCommand(): unknown message sublass: ', data.message_subclass);
      return this.replySolicitedStatus('Command Reject');
    }
  }

  /**
   * [processTransactionReply description]
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  processTransactionReply(data){    
    this.processState(data.next_state);

    if(data.screen_display_update)
      this.screens.parseScreenDisplayUpdate(data.screen_display_update);

    return this.replySolicitedStatus('Ready');
  }

  /**
   * [getMessageCoordinationNumber 
   *  Message Co-Ordination Number is a character assigned by the
   *  terminal to each transaction request message. The terminal assigns a
   *  different co-ordination number to each successive transaction request,
   *  on a rotating basis. Valid range of the co-ordination number is 31 hex
   *  to 3F hex, or if enhanced configuration parameter 34 ‘MCN Range’ has
   *  been set to 001, from 31 hex to 7E hex. Central must include the
   *  corresponding co-ordination number when responding with a
   *  Transaction Reply Command.
   *  
   *  This ensures that the Transaction Reply matches the Transaction
   *  Request. If the co-ordination numbers do not match, the terminal
   *  sends a Solicited Status message with a Command Reject status.
   *  Central can override the Message Co-Ordination Number check by
   *  sending a Co-Ordination Number of ‘0’ in a Transaction Reply
   *  command. As a result, the terminal does not verify that the
   *  Transaction Reply co-ordinates with the last transaction request
   *  message.]
   * @return {[type]} [description]
   */
  getMessageCoordinationNumber(){
    let saved = this.settings.get('message_coordination_number');
    if(!saved)
      saved = '0';

    saved = String.fromCharCode(saved.toString().charCodeAt(0) + 1);
    if(saved.toString().charCodeAt(0) > 126)
      saved = '1';

    this.settings.set('message_coordination_number', saved);
    return saved;
  }

  /**
   * [initBuffers clears the terminal buffers
   * When the terminal enters the Card Read State, the following buffers are initialized:
   *  - Card data buffers (no data)
   *  - PIN and General Purpose buffers (no data)
   *  - Amount buffer (zero filled)
   *  - Operation code buffer (space filled)
   *  - FDK buffer (zero filled)]
   * @return {[type]} [description]
   */
  initBuffers(){
    // In a real ATM PIN_buffer contains encrypted PIN, but in this application PIN_buffer contains clear PIN entered by cardholder.
    // To get the encrypted PIN, use getEncryptedPIN() method
    this.PIN_buffer = '';

    this.buffer_B = '';
    this.buffer_C = '';
    this.amount_buffer = '000000000000';
    this.opcode.init();
    this.FDK_buffer = '';   // FDK_buffer is only needed on state type Y and W to determine the next state

    return true;
  }

  /**
   * [processStateA process the Card Read state]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processStateA(state){
    this.initBuffers();
    this.display.setScreenByNumber(state.get('screen_number'));
    
    if(this.card)
      return state.get('good_read_next_state');
  }

  /**
   * [processPINEntryState description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processPINEntryState(state){
    /**
     * The cardholder enters the PIN, which can consist of from four to
     * sixteen digits, on the facia keyboard. If the cardholder enters fewer
     * than the number of digits specified in the FIT entry, PMXPN, he
     * must press FDK ‘A’ (or FDK ‘I’, if the option which enables the keys
     * to the left of the CRT is set) or the Enter key after the last digit has
     * been entered. Pressing the Clear key clears all digits.
     */
    this.display.setScreenByNumber(state.get('screen_number'));
    this.setFDKsActiveMask('001'); // Enabling button 'A' only

    if(this.PIN_buffer.length > 3){
      // TODO: PIN encryption
      return state.get('remote_pin_check_next_state');
    }
  }

  /**
   * [processAmountEntryState description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processAmountEntryState(state){
    this.display.setScreenByNumber(state.get('screen_number'));
    this.setFDKsActiveMask('015'); // Enabling 'A', 'B', 'C', 'D' buttons
    this.amount_buffer = '000000000000';

    let button = this.buttons_pressed.shift();
    if(this.isFDKButtonActive(button))
      return state.get('FDK_' + button + '_next_state');
  }

  /**
   * [processStateD description]
   * @param  {[type]} state           [description]
   * @param  {[type]} extension_state [description]
   * @return {[type]}                 [description]
   */
  processStateD(state, extension_state){
    //this.setBufferFromState(state, extension_state);
    this.opcode.setBufferFromState(state, extension_state);
    return state.get('next_state');
  }

  /**
   * [processFourFDKSelectionState description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processFourFDKSelectionState(state){
    this.display.setScreenByNumber(state.get('screen_number'));

    this.activeFDKs = [];
    ['A', 'B', 'C', 'D'].forEach((element, index) => {
      if(state.get('FDK_' + element + '_next_state') !== '255')
        this.activeFDKs.push(element);
    });

    let button = this.buttons_pressed.shift();
    if(this.isFDKButtonActive(button)){
      let index = parseInt(state.get('buffer_location'), 10);
      if(index < 8)
        this.opcode.setBufferValueAt(7 - index, button);
      else
        this.log.error('Invalid buffer location value: ' + state.get('buffer_location') + '. Operation Code buffer is not changed');

      return state.get('FDK_' + button + '_next_state');
    }
  }

  processInformationEntryState(state){
    this.display.setScreenByNumber(state.get('screen_number'));
    let active_mask = '0';
    [ state.get('FDK_A_next_state'),
      state.get('FDK_B_next_state'),
      state.get('FDK_C_next_state'),
      state.get('FDK_D_next_state')].forEach( element => {
      if(element !== '255')
        active_mask += '1';
      else
        active_mask += '0';
    });
    this.setFDKsActiveMask(active_mask);

    let button = this.buttons_pressed.shift();
    if(this.isFDKButtonActive(button)){
      return state.get('FDK_' + button + '_next_state');
    }

    switch(state.get('buffer_and_display_params')[2]){
    case '0':
    case '1':
      this.buffer_C = '';
      break;

    case '2':
    case '3':
      this.buffer_B = '';
      break;

    default: 
      this.log.error('Unsupported Display parameter value: ' + state.get('buffer_and_display_params')[2]);
    }
  }


  processInteractiveTransaction(request){
    this.interactive_transaction = false;

    // Keyboard data entered after receiving an Interactive Transaction Response is stored in General Purpose Buffer B
    let button = this.buttons_pressed.shift();
    if(this.isFDKButtonActive(button)){
      this.buffer_B = button;
      request.buffer_B = button;
    }
    return request;
  }


  /**
   * [processTransactionRequestState description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processTransactionRequestState(state){
    this.display.setScreenByNumber(state.get('screen_number'));

    let request = {
      message_class: 'Unsolicited',
      message_subclass: 'Transaction Request',
      top_of_receipt: '1',
      message_coordination_number: this.getMessageCoordinationNumber(),
    };

    if(!this.interactive_transaction){
      if(state.get('send_track2') === '001')
        request.track2 = this.track2;

      // Send Track 1 and/or Track 3 option is not supported 

      if(state.get('send_operation_code') === '001')
        request.opcode_buffer = this.opcode.getBuffer();

      if(state.get('send_amount_data') === '001')
        request.amount_buffer = this.amount_buffer;

      switch(state.get('send_pin_buffer')){
      case '001':   // Standard format. Send Buffer A
      case '129':   // Extended format. Send Buffer A
        request.PIN_buffer = this.crypto.getEncryptedPIN(this.PIN_buffer, this.card.number);
        break;
      case '000':   // Standard format. Do not send Buffer A
      case '128':   // Extended format. Do not send Buffer A
      default:
        break;
      }

      switch(state.get('send_buffer_B_buffer_C')){
      case '000': // Send no buffers
        break;

      case '001': // Send Buffer B
        request.buffer_B = this.buffer_B;
        break;

      case '002': // Send Buffer C
        request.buffer_C = this.buffer_C;
        break;

      case '003': // Send Buffer B and C
        request.buffer_B = this.buffer_B;
        request.buffer_C = this.buffer_C;
        break;

      default:
        // TODO: If the extended format is selected in table entry 8, this entry is an Extension state number.
        if(state.get('send_pin_buffer') in ['128', '129']){
          null;
        }
        break;
      }
    } else {
      request = this.processInteractiveTransaction(request);
    }
    this.transaction_request = request; // further processing is performed by the atm listener
  }

  /**
   * [processCloseState description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processCloseState(state){
    this.display.setScreenByNumber(state.get('receipt_delivered_screen'));
    this.setFDKsActiveMask('000');  // Disable all FDK buttons
    this.card = null;
    this.log.info(this.trace.object(state));
  }

  /**
   * [processStateK description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processStateK(state){
    let institution_id = this.FITs.getInstitutionByCardnumber(this.card.number);
    this.log.info('Card ' + this.card.number + ' matches with institution_id ' + institution_id);
    if(institution_id)
      return state.get('state_exits')[parseInt(institution_id, 10)];
    else
      this.log.error('Unable to get Financial Institution by card number');
  }

  /**
   * [processStateW description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processStateW(state){
    return state.get('states')[this.FDK_buffer];
  }


  /**
   * [setAmountBuffer assign the provide value to amount buffer]
   * @param {[type]} amount [description]
   */
  setAmountBuffer(amount){
    if(!amount)
      return;
    this.amount_buffer = this.amount_buffer.substr(amount.length) + amount;
  }


  /**
   * [processStateX description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processStateX(state, extension_state){
    this.display.setScreenByNumber(state.get('screen_number'));
    this.setFDKsActiveMask(state.get('FDK_active_mask'));

    let button = this.buttons_pressed.shift();
    if(this.isFDKButtonActive(button)){
      this.FDK_buffer = button;

      if(extension_state){
        /**
         * Each table entry contains a value that is stored in
         * the buffer specified in the associated FDK
         * Information Entry state table (table entry 7) if the
         * specified FDK or touch area is pressed.
         */
        let buffer_value;
        [null, null, 'A', 'B', 'C', 'D', 'F', 'G', 'H', 'I'].forEach((element, index) => {
          if(button === element)
            buffer_value = extension_state.get('entries')[index];
        });

        /**
         * Buffer ID identifies which buffer is to be edited and the number of zeros to add 
         * to the values specified in the Extension state:
         * 01X - General purpose buffer B
         * 02X - General purpose buffer C
         * 03X - Amount buffer
         * X specifies the number of zeros in the range 0-9
         */
        // Checking number of zeroes to pad
        let num_of_zeroes = state.get('buffer_id').substr(2, 1);
        for (let i = 0; i < num_of_zeroes; i++)
          buffer_value += '0';

        // Checking which buffer to use
        switch(state.get('buffer_id').substr(1, 1)){
        case '1':
          this.buffer_B = buffer_value;
          break;
  
        case '2':
          this.buffer_C = buffer_value;
          break;
  
        case '3':
          this.setAmountBuffer(buffer_value);
          break;
  
        default:
          this.log.error('Unsupported buffer id value: ' + state.get('buffer_id'));
          break;
        }
      }

      return state.get('FDK_next_state');
    }
  }

  /**
   * [processStateY description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processStateY(state, extension_state){
    this.display.setScreenByNumber(state.get('screen_number'));
    this.setFDKsActiveMask(state.get('FDK_active_mask'));

    if(extension_state){
      this.log.error('Extension state on state Y is not yet supported');
    }else{
      let button = this.buttons_pressed.shift();
      if(this.isFDKButtonActive(button)){
        this.FDK_buffer = button;

        // If there is no extension state, state.get('buffer_positions') defines the Operation Code buffer position 
        // to be edited by a value in the range 000 to 007.
        this.opcode.setBufferValueAt(parseInt(state.get('buffer_positions'), 10), button);
       
        return state.get('FDK_next_state');
      }
    }
  }

  /**
   * [processStateBeginICCInit description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processStateBeginICCInit(state){
    return state.get('icc_init_not_started_next_state');
  }

  /**
   * [processStateCompleteICCAppInit description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processStateCompleteICCAppInit(state){
    let extension_state = this.states.get(state.get('extension_state'));
    this.display.setScreenByNumber(state.get('please_wait_screen_number'));

    return extension_state.get('entries')[8]; // Processing not performed
  }

  /**
   * [processICCReinit description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processICCReinit(state){
    return state.get('processing_not_performed_next_state');
  }


  /**
   * [processSetICCDataState description]
   * @param  {[type]} state [description]
   * @return {[type]}       [description]
   */
  processSetICCDataState(state){
    // No processing as ICC cards are not currently supported
    return state.get('next_state');
  }


  /**
   * [processState description]
   * @param  {[type]} state_number [description]
   * @return {[type]}              [description]
   */
  processState(state_number){
    let state = this.states.get(state_number);
    let next_state = null;

    do{
      if(state){
        this.current_state = state;
        this.log.info('Processing state ' + state.get('number') + state.get('type') + ' (' + state.get('description') + ')');
      } else {
        this.log.error('Error getting state ' + state_number + ': state not found');
        return false;
      }
        
      switch(state.get('type')){
      case 'A':
        next_state = this.processStateA(state);
        break;

      case 'B':
        next_state = this.processPINEntryState(state);
        break;

      case 'D':
        state.get('extension_state') !== '255' ? next_state = this.processStateD(state, this.states.get(state.get('extension_state'))) : next_state = this.processStateD(state);
        break;

      case 'E':
        next_state = this.processFourFDKSelectionState(state);
        break;

      case 'F':
        next_state = this.processAmountEntryState(state);
        break;

      case 'H':
        next_state = this.processInformationEntryState(state);
        break;

      case 'I':
        next_state = this.processTransactionRequestState(state);
        break;

      case 'J':
        next_state = this.processCloseState(state);
        break;

      case 'K':
        next_state = this.processStateK(state);
        break;

      case 'X':
        (state.get('extension_state') !== '255' && state.get('extension_state') !== '000') ? next_state = this.processStateX(state, this.states.get(state.get('extension_state'))) : next_state = this.processStateX(state);
        break;

      case 'Y':
        (state.get('extension_state') !== '255' && state.get('extension_state') !== '000') ? next_state = this.processStateY(state, this.states.get(state.get('extension_state'))) : next_state = this.processStateY(state);
        break;

      case 'W':
        next_state = this.processStateW(state);
        break;

      case '+':
        next_state = this.processStateBeginICCInit(state);
        break;

      case '/':
        next_state = this.processStateCompleteICCAppInit(state);
        break;

      case ';':
        next_state = this.processICCReinit(state);
        break;

      case '?':
        next_state = this.processSetICCDataState(state);
        break;

      default:
        this.log.error('atm.processState(): unsupported state type ' + state.get('type'));
        next_state = null;
      }

      if(next_state)
        state = this.states.get(next_state);
      else
        break;

    }while(state);

    return true;
  }

  /**
   * [parseTrack2 parse track2 and return card object]
   * @param  {[type]} track2 [track2 string]
   * @return {[card object]} [description]
   */
  parseTrack2(track2){
    let card = {};
    try{
      let splitted = track2.split('=');
      card.track2 = track2;
      card.number = splitted[0].replace(';', '');
      card.service_code = splitted[1].substr(4, 3);
    }catch(e){
      this.log.info(e);
      return null;
    }

    return card;
  }

  readCard(cardnumber, track2_data){
    this.track2 = cardnumber + '=' + track2_data;
    this.card = this.parseTrack2(this.track2);
    if(this.card){
      this.log.info('Card ' + this.card.number + ' read');
      this.log.info('Track2: ' + this.track2);
      this.processState('000');
    }
    this.setStatus('Processing Card');
  }

  /**
   * [initCounters description]
   * @return {[type]} [description]
   */
  initCounters(){
    let config_id = this.settings.get('config_id');
    (config_id) ? this.setConfigID(config_id) : this.setConfigID('0000');

    this.supply_counters = {};
    this.supply_counters.tsn = '0000';
    this.supply_counters.transaction_count = '0000000';
    this.supply_counters.notes_in_cassettes = '00011000220003300044';
    this.supply_counters.notes_rejected = '00000000000000000000';
    this.supply_counters.notes_dispensed = '00000000000000000000';
    this.supply_counters.last_trxn_notes_dispensed = '00000000000000000000';
    this.supply_counters.card_captured = '00000';
    this.supply_counters.envelopes_deposited = '00000';
    this.supply_counters.camera_film_remaining = '00000';
    this.supply_counters.last_envelope_serial = '00000';
  }

  getSupplyCounters(){
    return this.supply_counters;
  }

  /**
   * [setConfigID description]
   * @param {[type]} config_id [description]
   */
  setConfigID(config_id){
    this.config_id = config_id;
    this.settings.set('config_id', config_id);
  }

  getConfigID(){
    return this.config_id;
  }

  setStatus(status){
    this.status = status;

    switch(status){
    case 'Offline':
    case 'Out-Of-Service':
      this.display.setScreenByNumber('001');
      break;
    }
  }

  /**
   * [processHostMessage description]
   * @param  {[type]} data [description]
   * @return {[type]}      [description]
   */
  processHostMessage(data){
    switch(data.message_class){
    case 'Terminal Command':
      return this.processTerminalCommand(data);

    case 'Data Command':
      return this.processDataCommand(data);

    case 'Transaction Reply Command':
      return this.processTransactionReply(data);
            
    case 'EMV Configuration':
      return this.replySolicitedStatus('Ready');

    default:
      this.log.info('ATM.processHostMessage(): unknown message class: ' + data.message_class);
      break;
    }
    return false;
  }

  /**
   * [processFDKButtonPressed description]
   * @param  {[type]} button [description]
   * @return {[type]}        [description]
   */
  processFDKButtonPressed(button){
    // log.info(button + ' button pressed');
    switch(this.current_state.get('type')){
    case 'B':
      if (button === 'A' && this.PIN_buffer.length >= 4)
        this.processState(this.current_state.get('number'));
      break;

    case 'H':
      {
        let active_mask = '0';
        [ this.current_state.get('FDK_A_next_state'),
          this.current_state.get('FDK_B_next_state'),
          this.current_state.get('FDK_C_next_state'),
          this.current_state.get('FDK_D_next_state')].forEach( element => {
          if(element !== '255')
            active_mask += '1';
          else
            active_mask += '0';
        });
        this.setFDKsActiveMask(active_mask);

        if(this.isFDKButtonActive(button)){
          this.buttons_pressed.push(button);
          this.processState(this.current_state.get('number'));
        }
      }
      break;

    default:
      // No special processing required
      this.buttons_pressed.push(button);
      this.processState(this.current_state.get('number'));
      break;
    }
  }

  processBackspaceButtonPressed(){
    switch(this.current_state.get('type')){
    case 'B':
      this.PIN_buffer = this.PIN_buffer.slice(0, -1);
      this.display.insertText(this.PIN_buffer, '*');
      break;
    case 'F':
      this.amount_buffer = '0' + this.amount_buffer.substr(0, this.amount_buffer.length - 1);
      this.display.insertText(this.amount_buffer);
      break;
    case 'H':
      {
        let buffer;
        let key;

        switch(this.current_state.get('buffer_and_display_params')[2]){
        case '0': // Display 'X' for each numeric key pressed. Store data in general-purpose Buffer C
          this.buffer_C = this.buffer_C.substr(0, this.buffer_C.length - 1);
          key = 'X';
          buffer = this.buffer_C;
          break;
        case '1': // Display data as keyed in. Store data in general-purpose Buffer C
          this.buffer_C = this.buffer_C.substr(0, this.buffer_C.length - 1);
          buffer = this.buffer_C;
          break;
        case '2': // Display 'X' for each numeric key pressed. Store data in general-purpose Buffer B
          this.buffer_B = this.buffer_B.substr(0, this.buffer_B.length - 1);          
          buffer = this.buffer_B;
          key = 'X';
          break;
        case '3': // Display data as keyed in. Store data in general-purpose Buffer B
          this.buffer_B = this.buffer_B.substr(0, this.buffer_B.length - 1);
          buffer = this.buffer_B;
          break;
        }
        this.display.insertText(buffer, key);
      }
      break;
    default:
      break;
    }
  }

  processEnterButtonPressed(){
    switch(this.current_state.get('type')){
    case 'B':
      if(this.PIN_buffer.length >= 4)
        this.processState(this.current_state.get('number'));
      this.display.insertText(this.PIN_buffer, '*');
      break;
    case 'F':
      // If the cardholder presses the Enter key, it has the same effect as pressing FDK ‘A’
      this.buttons_pressed.push('A');
      this.processState(this.current_state.get('number'));
      break;
    default:
      break;
    }
  }

  processEscButtonPressed(){
    switch(this.current_state.get('type')){
    case 'B':
      this.PIN_buffer = '';
      break;
    default:
      break;
    }
  }

  processNumericButtonPressed(button){
    switch(this.current_state.get('type')){
    case 'B':
      this.PIN_buffer += button;
      if(this.PIN_buffer.length >= this.FITs.getMaxPINLength(this.card.number) || this.PIN_buffer.length >= 6 )
        this.processState(this.current_state.get('number'));
      this.display.insertText(this.PIN_buffer, '*');
      break;
    case 'F':
      this.amount_buffer = this.amount_buffer.substr(1) + button;
      this.display.insertText(this.amount_buffer);
      break;
    case 'H':
      {
        let key;
        let buffer;
        let display_param = this.current_state.get('buffer_and_display_params')[2];
        switch(display_param){
        case '0': // Display 'X' for each numeric key pressed. Store data in general-purpose Buffer C
        case '1': // Display data as keyed in. Store data in general-purpose Buffer C
          if(this.buffer_C.length < 32){
            this.buffer_C += button;

            buffer = this.buffer_C;
            if(display_param === '0'){
              key = 'X';
            }
          }
          break;
        case '2': // Display 'X' for each numeric key pressed. Store data in general-purpose Buffer B
        case '3': // Display data as keyed in. Store data in general-purpose Buffer B
          if(this.buffer_B.length < 32){
            this.buffer_B += button;

            buffer = this.buffer_B;
            if(  display_param === '2'){
              key = 'X';
            }
          }
          break;
        default:
          this.log.error('Unsupported Display parameter value: ' + display_param);
        }
       
        // console.log(buffer);
        if(buffer)
          this.display.insertText(buffer, key);
      }
      break;
    default:
      this.log.error('No keyboard entry allowed for state type ' + this.current_state.get('type'));
      break;
    }
  }

  /**
   * [processPinpadButtonPressed description]
   * @param  {[type]} button [description]
   * @return {[type]}        [description]
   */
  processPinpadButtonPressed(button){
    switch(button){
    case 'backspace':
      this.processBackspaceButtonPressed();
      break;
    case 'enter':
      this.processEnterButtonPressed();
      break;
    case 'esc':
      this.processEscButtonPressed();
      break;
    default:
      this.processNumericButtonPressed(button);
    }
  }

  getCurrentState(){
    return this.current_state;
  }
}

module.exports = ATM;