Source: Clover.js

module.exports.Clover = Clover;

var CloverOAuth = require("./CloverOAuth.js");
var CloverError = require("./CloverError.js");
var CardEntryMethods = require("./CardEntryMethods.js");
var WebSocketDevice = require("./WebSocketDevice.js");
var RemoteMessageBuilder = require("./RemoteMessageBuilder.js");
var LanMethod = require("./LanMethod.js");
var XmlHttpSupport = require("./xmlHttpSupport.js");
var Endpoints = require("./Endpoints.js");
var CloverID = require("./CloverID.js");
var KeyPress = require("./KeyPress.js");
var VoidReason = require("./VoidReason.js");

var Logger = require('./Logger.js');

// !!NOTE!!  The following is automatically updated to reflect the npm version.
// See the package.json postversion script, which maps to scripts/postversion.sh
// Do not change this or the versioning may not reflect the npm version correctly.
CLOVER_CLOUD_SDK_VERSION = "1.1.0";

/**
 * Clover API for external Systems
 *
 * @deprecated - use the {remotepay.ICloverConnector} and {remotepay.ICloverConnectorListener} instead
 * @param {CloverConfig} [configurationIN] - the device that sends and receives messages
 * @constructor
 */
function Clover(configurationIN) {
    this.log = Logger.create();
    var configuration= {};
    if(configurationIN){
        // Make sure we do not change the passed object, make a copy.
        configuration=JSON.parse(JSON.stringify(configurationIN));
    }
    this.persistanceHandler = null; // new CookiePersistance();

    this.debugConfiguration = {};
    this.configuration = configuration;
    if (!this.configuration) {
        this.configuration = {};
    }
    if(this.configuration.debugConfiguration) {
      this.debugConfiguration = this.configuration.debugConfiguration
    } else {
      this.debugConfiguration = {};
    }
    this.configuration.allowOvertakeConnection =
        Boolean(this.configuration["allowOvertakeConnection"]);

    this.device = new WebSocketDevice(this.configuration.allowOvertakeConnection, this.configuration["friendlyId"]);

    if(!this.configuration["clientId"]) {
        throw new CloverError(CloverError.INCOMPLETE_CONFIGURATION, "'clientId' must be included in the configuration.");
    }
    this.device.messageBuilder = new RemoteMessageBuilder("com.clover.remote.protocol.websocket",
      "com.clover.cloverconnector.cloud:" + CLOVER_CLOUD_SDK_VERSION,
      this.configuration.clientId
    );

    // Echo all messages sent and received.
    this.device.echoAllMessages = false;
    this.pauseBetweenDiscovery = 3000;
    this.numberOfDiscoveryMessagesToSend = 10;
    // This is used to augment the 'isOpen' functionality.
    this.discoveryResponseReceived = false;

    /*
     Set up a value to help the user of the Clover object know when it is available.
     */
    this._isOpen = false;
    this.device.on(WebSocketDevice.DEVICE_OPEN, function () {
        this._isOpen = true;
    }.bind(this));
    this.device.on(WebSocketDevice.DEVICE_CLOSE, function () {
        this._isOpen = false;
        this.configuration.deviceURL = null;
        this.discoveryResponseReceived = false;
    }.bind(this));
    this.device.on(WebSocketDevice.DEVICE_ERROR, function () {
        this._isOpen = false;
        this.device.forceClose();
    }.bind(this));
    this.device.on(WebSocketDevice.CONNECTION_ERROR, function () {
        this._isOpen = false;
        this.device.forceClose();
    }.bind(this));
    this.device.on(WebSocketDevice.CONNECTION_STOLEN, function () {
        this._isOpen = false;
        this.device.forceClose();
    }.bind(this));

    /**
     * @returns {boolean} true if the device is open and ready for communications,
     *  false if the device is closed, or has an error.
     */
    this.isOpen = function () {
        // The device must not only be opened, it must have responded with a discovery response
        // to indicate that not only is the socket communication channel open, but the device
        // has bootstrapped the functionality to actually respond to messages.
        return this._isOpen && this.discoveryResponseReceived;
    };

    /*
     The following is a bit elaborate, but I want it to be clear that the default of
     this value is 'true', and that it is only false if explicitly set.
     */
    if (this.configuration.hasOwnProperty("autoVerifySignature") &&
        this.configuration.autoVerifySignature != null &&
        this.configuration.autoVerifySignature === false) {
        this.configuration.autoVerifySignature = false;
    } else {
        this.configuration.autoVerifySignature = true;
    }

    this.configuration.disableRestartTransactionWhenFailed =
        Boolean(this.configuration.disableRestartTransactionWhenFailed);

    this.configuration.remotePrint =
        Boolean(this.configuration.remotePrint);

    /**
     * @private
     * @returns {{action: string, transactionType: string, transactionSubType: string, taxAmount: number, cardEntryMethods: *, disableRestartTransactionWhenFailed: boolean, remotePrint: boolean}}
     */
    this.sale_payIntentTemplate = function () {
        return {
            "action": "com.clover.remote.protocol.action.START_REMOTE_PROTOCOL_PAY",
            "transactionType": "PAYMENT",
            "taxAmount": 0, // tax amount is included in the amount
            "cardEntryMethods": CardEntryMethods.ALL,
            "disableRestartTransactionWhenFailed": this.configuration.disableRestartTransactionWhenFailed,
            "remotePrint": this.configuration.remotePrint
        };
    };

    /**
     * @private
     * @returns {{action: string, transactionType: string, transactionSubType: string, taxAmount: number, cardEntryMethods: *, disableRestartTransactionWhenFailed: boolean, remotePrint: boolean}}
     */
    this.auth_payIntentTemplate = function () {
        return {
            "action": "com.clover.remote.protocol.action.START_REMOTE_PROTOCOL_PAY",
            "transactionType": "PAYMENT",
            "taxAmount": 0, // tax amount is included in the amount
            "cardEntryMethods": CardEntryMethods.ALL,
            "disableRestartTransactionWhenFailed": this.configuration.disableRestartTransactionWhenFailed,
            "remotePrint": this.configuration.remotePrint
        };
    };

    /**
     * @private
     * @returns {{action: string, transactionType: string, taxAmount: number, cardEntryMethods: *, disableRestartTransactionWhenFailed: boolean, remotePrint: boolean}}
     */
    this.preAuth_payIntentTemplate = function () {
        return {
            "action": "com.clover.remote.protocol.action.START_REMOTE_PROTOCOL_PAY",
            "transactionType": "AUTH",
            "taxAmount": 0, // tax amount is included in the amount
            "cardEntryMethods": CardEntryMethods.ALL,
            "disableRestartTransactionWhenFailed": this.configuration.disableRestartTransactionWhenFailed,
            "remotePrint": this.configuration.remotePrint
        };
    };

    /**
     * @private
     * @type {{action: string, transactionType: string, taxAmount: number, cardEntryMethods: *, disableRestartTransactionWhenFailed: boolean, remotePrint: boolean}}
     */
    this.refund_payIntentTemplate = function() {
        return {
            "action": "com.clover.remote.protocol.action.START_REMOTE_PROTOCOL_PAY",
            "transactionType": "CREDIT",
            "taxAmount": 0, // tax amount is included in the amount
            "cardEntryMethods": CardEntryMethods.ALL,
            "disableRestartTransactionWhenFailed": this.configuration.disableRestartTransactionWhenFailed,
            "remotePrint": this.configuration.remotePrint
        };
    };

    //****************************************
    // Very useful for debugging
    //****************************************
    if (this.debugConfiguration[WebSocketDevice.ALL_MESSAGES]) {
        var me = this;
        this.device.on(WebSocketDevice.ALL_MESSAGES,
            function (message) {
                if ((message['type'] != 'PONG') && (message['type'] != 'PING')) {
                    me.log.debug(message);
                }
            }
        );
    }

    /**
     * Returns true if the signature should be automatically verified
     * @private
     * @returns {boolean}
     */
    this.getAutoVerifySignature = function () {
        if (this.configuration.hasOwnProperty("autoVerifySignature") &&
            this.configuration.autoVerifySignature != null &&
            this.configuration.autoVerifySignature === false) {
            return false;
        }
        return true;
    };

    /**
     * Closes the connection to the Clover device.
     */
    this.close = function () {
        if (this.device) {
            if (this.device.discoveryTimerId) {
                clearInterval(this.device.discoveryTimerId);
            }
            this.sendCancel();
            this.device.disconnectFromDevice();
        }
    };

    /**
     * Called to initialize the device for communications.
     *
     *  The device connection is NOT made on completion of this call.  The device connection
     *  will be made once the WebSocketDevice.onopen is called.
     *
     *  If there is no active configuration, then this will try to load the configuration using
     *  Clover.loadPersistedConfiguration.  Once the deviceURL has been derived, an attempt will be made to persist
     *  the configuration using Clover.persistConfiguration.
     *
     *  If there is not enough configuration information to connect to the device, then Clover.incompleteConfiguration
     *  will be called.
     *
     *  If the device is not connected to the push server, the callback will be called with the first parameter of a
     *  CloverError with a code of CloverError.DEVICE_OFFLINE.
     *
     *  If there is a communication error retrieving the device websocket connection point or device info, the callback
     *  will be called with the first parameter of a CloverError with a code of CloverError.COMMUNICATION_ERROR.
     *
     *  If the configuration of the device is incomplete, the callback will be called with the first parameter of a
     *  CloverError with a code of CloverError.INCOMPLETE_CONFIGURATION.
     *
     *  If the device is contacted, but does not respond to a discovery request, the callback will be called with the first parameter of a
     *  CloverError with a code of CloverError.DISCOVERY_TIMEOUT.
     *
     * @param callBackOnDeviceReady - callback function called when the device is ready for operations.  The
     *  callback function can be called with several possible results.
     *
     *  Adheres to error first paradigm.
     */
    this.initDeviceConnection = function (callBackOnDeviceReady) {
        if (!this.isOpen()) {
            if (callBackOnDeviceReady) {
                this.device.once(LanMethod.DISCOVERY_RESPONSE, function (message) {
                    callBackOnDeviceReady(null, message)
                });
            }
            return this.initDeviceConnectionInternal(callBackOnDeviceReady);
        }
        else {
            callBackOnDeviceReady();
        }
    };

    /**
     * Called to initialize the device for communications.
     *
     *  The device connection is NOT made on completion of this call.  The device connection
     *  will be made once the WebSocketDevice.onopen is called.
     * @private
     * @param callBackOnDeviceReady - callback function called when the device is ready for operations.
     */
    this.initDeviceConnectionInternal = function (callBackOnDeviceReady) {
        // Check to see if we have any configuration at all.
        if (!this.configuration) {
            if (!this.loadPersistedConfiguration(callBackOnDeviceReady)) {
                return;
            }
            this.initDeviceConnectionInternal(callBackOnDeviceReady);
        } else if (this.configuration.deviceURL) {
            // We have enough information to contact the device.
            // save the configuration (except for the device url, which always changes)
            // in a cookie.
            this.persistConfiguration();

            // We have the device url, contact the device
            this.contactDevice(callBackOnDeviceReady);
        } else {
            // Otherwise we must have the oauth token to get the information we need at the very least.
            // Either we already have it...
            if (this.configuration.oauthToken) {
                // We need the access token, the domain and the merchantId in order to get the devices
                // We already know that we have the token, but we need to check for the
                // domain and merchantId.
                if(!this.configuration.merchantId) {
                    this.configuration.merchantId = this.cloverOAuth.getURLParams()["merchant_id"];
                }
                if(!this.configuration.merchantId) {
                    // We do not have enough info to initialize.  Error out
                    this.incompleteConfiguration("Incomplete init info, missing 'merchantId'", this.configuration,
                        callBackOnDeviceReady);
                    return;
                }
                if (this.configuration.domain) {
                    // We need the device id of the device we will contact.
                    // Either we have it...
                    var xmlHttpSupport = new XmlHttpSupport();
                    var inlineEndpointConfig = {"configuration": {}};
                    var me = this;
                    inlineEndpointConfig.getAccessToken = function () {
                        return me.configuration.oauthToken;
                    };
                    inlineEndpointConfig.configuration.domain = this.configuration.domain;
                    var endpoints = new Endpoints(inlineEndpointConfig);

                    if (this.configuration.deviceId) {
                        var noDashesDeviceId = this.configuration.deviceId.replace(/-/g, "");
                        // this is the uuid for the device
                        xmlHttpSupport = new XmlHttpSupport();
                        // This is the data posted to tell the server we want to create a connection
                        // to the device
                        var deviceContactInfo = {
                            deviceId: noDashesDeviceId,
                            isSilent: true
                        };

                        xmlHttpSupport.postData(endpoints.getAlertDeviceEndpoint(this.configuration.merchantId),
                            function (data) {
                                me.device.bootStrapReconnect = function(callback) {
                                    xmlHttpSupport.postData(endpoints.getAlertDeviceEndpoint(me.configuration.merchantId),
                                        callback, callback, deviceContactInfo);
                                };
                                // The format of the data received is:
                                //{
                                //    'sent': true | false,
                                //    'host': web_socket_host,
                                //    'token': token_to_link_to_the_device
                                //}
                                // Use this data to build the web socket url
                                // Note "!data.hasOwnProperty('sent')" is included to allow for
                                // backwards compatibility.  If the property is NOT included, then
                                // we will assume an earlier version of the protocol on the server,
                                // and assume that the notification WAS SENT.
                                if (!data.hasOwnProperty('sent') || data.sent) {
                                    var url = data.host + Endpoints.WEBSOCKET_PATH + '?token=' + data.token;
                                    me.device.messageBuilder = new RemoteMessageBuilder(
                                        "com.clover.remote.protocol.websocket");
                                    if(!me.configuration["clientId"]) {
                                        throw new CloverError(CloverError.INCOMPLETE_CONFIGURATION, "'clientId' must be included in the configuration.");
                                    }
                                    me.device.messageBuilder = new RemoteMessageBuilder("com.clover.remote.protocol.websocket",
                                      "com.clover.cloverconnector.cloud:" + CLOVER_CLOUD_SDK_VERSION,
                                      me.configuration.clientId
                                    );

                                    me.log.debug("Server responded with information on how to contact device. " +
                                        "Opening communication channel...");

                                    // The response to this will be reflected in the device.onopen method (or on error),
                                    // That function will attempt the discovery.
                                    me.configuration.deviceURL = url;
                                    //recurse
                                    me.initDeviceConnectionInternal(callBackOnDeviceReady);
                                } else {
                                    // Should it retry?
                                    // If the callback is defined, call it.
                                    var message = "Device is not connected to push server, cannot create connection";
                                    if (callBackOnDeviceReady) {
                                        callBackOnDeviceReady(new CloverError(CloverError.DEVICE_OFFLINE,
                                            message));
                                    } else {
                                        me.log.error(message);
                                    }
                                }
                            },
                            function (error) {
                                if (callBackOnDeviceReady) {
                                    callBackOnDeviceReady(new CloverError(CloverError.COMMUNICATION_ERROR,
                                        "Error getting device ws endpoint", error));
                                } else {
                                    me.log.error(error);
                                }
                            }, deviceContactInfo
                        );

                    } else /* if (this.configuration.deviceSerialId) */ {
                        // or we need to go get it.  This is a little hard, because the merchant
                        // can have multiple devices.

                        // If there are multiple devices, we need to know which device the user wants
                        // to use.  They can pass the 'serial' number of the device, or a 0 - based index
                        // for the devices, which assumes they know what order the device list will be
                        // returned in.
                        var url = endpoints.getDevicesEndpoint(this.configuration.merchantId);

                        xmlHttpSupport.getData(url,
                            function (devices) {
                                me.log.debug(devices);
                                me.handleDevices(devices);
                                if (me.configuration.deviceSerialId) {
                                    // Stations do not support the kiosk/pay display.
                                    // If the user has selected one, then print out a (loud) warning
                                    var myDevice = me.deviceBySerial[me.configuration.deviceSerialId];
                                    if (null == myDevice) {
                                        callBackOnDeviceReady(new CloverError(CloverError.DEVICE_NOT_FOUND,
                                            "Device " + deviceSerialId + " not in set returned."));
                                    }
                                    if (me.deviceBySerial[me.configuration.deviceSerialId].model == "Clover_C100") {
                                        me.log.warn(
                                            "Warning - Selected device model (" +
                                            me.deviceBySerial[me.configuration.deviceSerialId].model +
                                            ") does not support cloud pay display." +
                                            "  Will attempt to send notification to device, but no response" +
                                            " should be expected.");
                                    }
                                    // serial' number of the device
                                    me.configuration.deviceId =
                                        me.deviceBySerial[me.configuration.deviceSerialId].id;
                                    // recurse
                                    me.initDeviceConnectionInternal(callBackOnDeviceReady);
                                } else {
                                    //Nothing left to try.  Either error out or get more info from the user.
                                    me.incompleteConfiguration("Cannot determine what device to use for connection." +
                                        "  You must provide the configuration.deviceId, or the serial number" +
                                        " of the device. " +
                                        " You can find the device serial number using the device. Select " +
                                        "'Settings > About (Station|Mini|Mobile) > Status', select 'Status' and " +
                                        "look for 'Serial number' in the list displayed.  The list of devices found " +
                                        "for this configuration are " + JSON.stringify(devices), me.configuration,
                                        callBackOnDeviceReady);
                                    return;
                                }
                            }
                            , function (error) {
                                if (callBackOnDeviceReady) {
                                    callBackOnDeviceReady(new CloverError(CloverError.COMMUNICATION_ERROR,
                                        "Error getting device information", error));
                                } else {
                                    me.log.error(error);
                                }
                            }
                        );
                    }
                } else {
                    // We do not have enough info to initialize.  Error out
                    this.incompleteConfiguration("Incomplete init info, missing 'domain'.", this.configuration,
                        callBackOnDeviceReady);
                    return;
                }
            } else {
                // or we need to go get it.
                // If we need to go get it, then we will need the clientId
                // and the domain
                if (this.configuration.clientId && this.configuration.domain) {
                    this.cloverOAuth = new CloverOAuth(this.configuration);
                    // This may cause a redirect
                    this.persistConfiguration();
                    var me = this;
                    this.cloverOAuth.getAccessToken(
                        // recurse
                        function(token) {
                            me.configuration.oauthToken = token;
                            me.initDeviceConnectionInternal(callBackOnDeviceReady);
                        }
                    );
                } else {
                    if (!this.loadPersistedConfiguration(callBackOnDeviceReady)) {
                        return;
                    }
                    this.initDeviceConnectionInternal(callBackOnDeviceReady);
                }
            }
        }
    };

    /**
     * Loads the configuration that was stored.  This implementation just grabs the
     * configuration from a cookie.  Reimplementation of this function could provide
     * configuration from a server or even a UI.
     *
     * @param callback - the error first callback that will be passed to Clover.incompleteConfiguration
     *  if no configuration could be loaded.
     * @returns {boolean} true if the configuration was loaded.
     */
    this.loadPersistedConfiguration = function (callback) {
        if(this.persistanceHandler) {
            var configuration = this.persistanceHandler.load();
            if(configuration) {
                this.configuration = configuration;
            }
            return this.configuration;
        }
        return false;
    };

    /**
     * Stores the configuration for later retrieval.  This implementation just drops the
     * configuration into a cookie.  Reimplementation of this function could provide
     * server or some other type of cloud persistence.
     */
    this.persistConfiguration = function () {
        if(this.persistanceHandler) {
            this.configuration = this.persistanceHandler.persist(this.configuration);
        }
    };

    /**
     * This can be overridden to provide any missing configuration information to this.configuration (
     * through a UI or a call to the server, etc...), after which a call to
     * Clover.initDeviceConnection would need to be made to continue attempting to connect to the device.
     *
     * Note that if an overridden implementation does NOT properly provide configuration, and a call is made to
     * Clover.initDeviceConnection, there is the possibility of creating a recursive loop that would
     * impact the performance of the application, and could make the browser unstable.
     *
     * Any override of this function should ensure that configuration information is complete before making
     * the call to Clover.initDeviceConnection.
     *
     * @param {string} message - an error message.  This could be ignored.
     * @param {CloverConfig} [configuration] - the configuration that is incomplete.
     * @param callback - the error first callback.  If defined, then it will be called with
     *  the first parameter as a CloverError.  If not defined, then the CloverError will be thrown.
     */
    this.incompleteConfiguration = function (message, configuration, callback) {
        // If this is used to obtain the configuration information, then the
        // configuration should be updated, and then the 'initDeviceConnection'
        // should be called again to connect to the device.
        var error = new CloverError(CloverError.INCOMPLETE_CONFIGURATION,
            message + ". Configuration is " + JSON.stringify(configuration, null, 4));
        if (callback) {
            callback(error);
        } else {
            throw error;
        }
    };

    /**
     * Handle a set of devices.  Sets up an internal map of devices from serial->device
     * @private
     * @param devicesVX
     */
    this.handleDevices = function (devicesVX) {
        var devices = null;
        this.deviceBySerial = {};
        // depending on the version of the call, the devices might be in a slightly different format.
        // We would need to determine what devices were capable of doing what we want.  This means we
        // need to know if the device has the websocket connection enabled.  The best way to do this is
        // to make a different REST call, but we could filter the devices here.
        if (devicesVX['devices']) {
            devices = devicesVX.devices;
        }
        else if (devicesVX['elements']) {
            devices = devicesVX.elements;
        }
        if(devices) {
            var i;
            for (i = 0; i < devices.length; i++) {
                this.deviceBySerial[devices[i].serial] = devices[i];
            }
        }
    };

    /**
     * Contacts the device.  Sends DISCOVERY_REQUEST messages for a period of time until a response is received.
     * Currently set to retry 10 times at 3 second intervals
     *
     * @private
     */
    this.contactDevice = function (callBackOnDeviceReady) {
        var me = this;
        this.discoveryResponseReceived = false;

        // Holds all the callbacks so that they can be removed later, if
        // They need to be.  Callbacks need to be removed in the 'end states'
        // that do not visit ALL the callback.
        var allCallBacks = [];

        var discoveryResponseCallback = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);
            // This id is set when the discovery request is sent when the device 'onopen' is called.
            clearInterval(me.device.discoveryTimerId);
            me.device.discoveryTimerId = null;
            me.discoveryResponseReceived = true;
            me.log.debug("Device has responded to discovery message.");
        };
        this.device.once(LanMethod.DISCOVERY_RESPONSE, discoveryResponseCallback);

        allCallBacks.push({"event": LanMethod.DISCOVERY_RESPONSE, "callback": discoveryResponseCallback});

        var onopenCallback = function () {
            // The connection to the device is open, but we do not yet know if there is anyone at the other end.
            // Send discovery request messages until we get a discovery response.
            me.device.dicoveryMessagesSent = 0;
            if (!me.device.discoveryTimerId) {
                me.device.discoveryTimerId =
                    setInterval(
                        function () {
                            me.log.debug("Sending 'discovery' message to device.");
                            try {
                                me.device.sendMessage(me.device.messageBuilder.buildDiscoveryRequest());
                            } catch (e) {
                                me.log.debug(e);
                            }
                            me.device.dicoveryMessagesSent++;

                            // Arbitrary decision that 10 messages is long enough to wait.
                            if (me.device.dicoveryMessagesSent > me.numberOfDiscoveryMessagesToSend) {
                                // Remove obsolete listeners.  This is an end state
                                me.device.removeListeners(allCallBacks);
                                var seconds = (me.numberOfDiscoveryMessagesToSend * me.pauseBetweenDiscovery) / 1000;
                                var message = "No discovery response after " + seconds + " seconds";
                                me.log.info(message +
                                    "  Shutting down the connection.");
                                me.device.disconnectFromDevice();
                                clearInterval(me.device.discoveryTimerId);
                                me.device.discoveryTimerId = null;
                                if (callBackOnDeviceReady) {
                                    callBackOnDeviceReady(new CloverError(CloverError.DISCOVERY_TIMEOUT,
                                        "No discovery response after 30 seconds"));
                                }
                            }
                        }, me.pauseBetweenDiscovery
                    );
            }
            me.log.info('device opened');
        };
        this.device.once(WebSocketDevice.DEVICE_OPEN, onopenCallback);
        allCallBacks.push({"event": WebSocketDevice.DEVICE_OPEN, "callback": onopenCallback});

        var connctionDeniedCallback = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);
            if (callBackOnDeviceReady) {
                callBackOnDeviceReady(new CloverError(CloverError.CONNECTION_DENIED,
                    message), message);
            }
        };
        this.device.once(WebSocketDevice.CONNECTION_DENIED, connctionDeniedCallback);
        allCallBacks.push({"event": WebSocketDevice.CONNECTION_DENIED, "callback": connctionDeniedCallback});

        me.log.info("Contacting device at " + this.configuration.deviceURL);
        this.device.contactDevice(this.configuration.deviceURL);
    };

    /**
     * Utility function to ensure that the external payment id is set.
     *
     * @private
     * @param txInfo
     * @returns {*}
     */
    this.ensureExternalPaymentId = function (txInfo) {
        var externalPaymentId = null;
        if (txInfo.hasOwnProperty('requestId') && txInfo.requestId != null) {
            externalPaymentId = txInfo.requestId;
        } else {
            externalPaymentId = CloverID.getNewId();
        }
        txInfo.externalPaymentId = externalPaymentId;
        return txInfo;
    };

    /**
     * Sale AKA purchase
     *
     * @param {TransactionRequest} saleInfo - the information for the sale
     * @param {Clover~transactionRequestCallback} saleRequestCallback - the callback that receives the sale completion
     *  information.
     * @return {string} paymentId - identifier used for the payment once the transaction is completed.  This may be
     *  passed in as part of the saleInfo.  If it is not passed in then this will be a new generated value.
     */
    this.sale = function (saleInfo, saleRequestCallback) {
        this.ensureExternalPaymentId(saleInfo);
        // For a sale, force the tip to be set
        if (!saleInfo.hasOwnProperty("tipAmount")) {
            saleInfo["tipAmount"] = 0;
        }
        if (this.verifyValidAmount(saleInfo, saleRequestCallback)) {
            this.internalTx(saleInfo, saleRequestCallback, this.sale_payIntentTemplate(), "payment");
        }
        return saleInfo.externalPaymentId;
    };

    /**
     * Auth
     *
     * @param {TransactionRequest} saleInfo - the information for the sale
     * @param {Clover~transactionRequestCallback} saleRequestCallback - the callback that receives the sale completion
     *  information.
     * @return {string} paymentId - identifier used for the payment once the transaction is completed.  This may be
     *  passed in as part of the saleInfo.  If it is not passed in then this will be a new generated value.
     */
    this.auth = function (saleInfo, saleRequestCallback) {
        this.ensureExternalPaymentId(saleInfo);
        // For a auth, force the tip to be set to null
        if (saleInfo.hasOwnProperty("tipAmount")) {
            delete saleInfo.tipAmount;
        }
        if (this.verifyValidAmount(saleInfo, saleRequestCallback)) {
            this.internalTx(saleInfo, saleRequestCallback, this.auth_payIntentTemplate(), "payment", true);
        }
        return saleInfo.externalPaymentId;
    };

    /**
     * Pre Auth
     *
     * @param {TransactionRequest} saleInfo - the information for the sale
     * @param {Clover~transactionRequestCallback} saleRequestCallback - the callback that receives the sale completion
     *  information.
     * @return {string} paymentId - identifier used for the payment once the transaction is completed.  This may be
     *  passed in as part of the saleInfo.  If it is not passed in then this will be a new generated value.
     */
    this.preAuth = function (saleInfo, saleRequestCallback) {
        this.ensureExternalPaymentId(saleInfo);
        delete saleInfo.tipAmount;
        if (this.verifyValidAmount(saleInfo, saleRequestCallback)) {
            this.internalTx(saleInfo, saleRequestCallback, this.preAuth_payIntentTemplate(), "payment");
        }
        return saleInfo.externalPaymentId;
    };

    /**
     * Refund AKA credit
     *
     * @param {TransactionRequest} refundInfo - amount is refunded
     * @param {Clover~transactionRequestCallback} refundRequestCallback
     */
    this.refund = function (refundInfo, refundRequestCallback) {
        if (this.verifyValidAmount(refundInfo, refundRequestCallback)) {
            refundInfo.amount = Math.abs(refundInfo.amount) * -1;
            this.internalTx(refundInfo, refundRequestCallback, this.refund_payIntentTemplate(), "credit");
        }
    };

    /**
     *
     * @private
     * @param txnInfo
     * @param errorFirstCallback
     * @returns {boolean}
     */
    this.verifyValidAmount = function (txnInfo, errorFirstCallback) {
        if (!txnInfo.hasOwnProperty("amount") || !Clover.isInt(txnInfo.amount) || (txnInfo.amount < 0)) {
            errorFirstCallback(CloverError.INVALID_DATA,
                new CloverError("paymentInfo must include 'amount',and  the value must be an integer with " +
                    "a value greater than 0"));
            return false;
        }
        return true;
    };

    /**
     * @private
     * @param {TransactionRequest} txnInfo
     * @param {Clover~transactionRequestCallback} txnRequestCallback
     * @param template
     * @param {string} txnName
     * @param {boolean} suppressOnScreenTips
     */
    this.internalTx = function (txnInfo, txnRequestCallback, template, txnName, suppressOnScreenTips) {
        // Use a template to start with
        var payIntent = template;
        if (txnInfo.hasOwnProperty("tipAmount") && !Clover.isInt(txnInfo.tipAmount)) {
            txnRequestCallback(new CloverError(CloverError.INVALID_DATA,
                "if paymentInfo has 'tipAmount', the value must be an integer"));
            return;
        }
        if (txnInfo.hasOwnProperty("taxAmount")) {
            if(!Clover.isInt(txnInfo.taxAmount)) {
                txnRequestCallback(new CloverError(CloverError.INVALID_DATA,
                    "if paymentInfo has 'taxAmount', the value must be an integer"));
                return;
            } else {
                payIntent.taxAmount = txnInfo.taxAmount;
            }
        }
        if (txnInfo.hasOwnProperty("tippableAmount")) {
            if (!Clover.isInt(txnInfo.tippableAmount)) {
                txnRequestCallback(new CloverError(CloverError.INVALID_DATA,
                    "if paymentInfo has 'tippableAmount', the value must be an integer"));
                return;
            } else {
                payIntent.tippableAmount = txnInfo.tippableAmount;
            }
        }
        if (txnInfo.hasOwnProperty("employeeId")) {
            payIntent.employeeId = txnInfo.employeeId;
        }
        if (txnInfo.hasOwnProperty("vaultedCard")) {
            payIntent.vaultedCard = txnInfo.vaultedCard;
        }
        if (txnInfo.hasOwnProperty("externalPaymentId")) {
            if (txnInfo.externalPaymentId.length > 32) {
                var error = new CloverError(CloverError.INVALID_DATA, "id is invalid - '" + txnInfo.externalPaymentId + "'");
                txnRequestCallback(error, null);
                return;
            }
            payIntent.externalPaymentId = txnInfo.externalPaymentId;
        }
        /*
         The order id cannot be specified at this time.
         if (txnInfo.hasOwnProperty("orderId")) {
         payIntent.orderId = txnInfo.orderId;
         }
         */
        var autoVerifySignature = this.getAutoVerifySignature();
        if (txnInfo.hasOwnProperty("autoVerifySignature")) {
            if (txnInfo.autoVerifySignature === true) {
                autoVerifySignature = true;
            }
        }
        payIntent.amount = txnInfo.amount;
        payIntent.tipAmount = txnInfo["tipAmount"];

        // Reserve a reference to this object
        var me = this;
        // We will hold on to the signature since we are not showing it to the user.
        var signature = null;

        // Holds all the callbacks so that they can be removed later, if
        // They need to be.  Callbacks need to be removed in the 'end states'
        // that do not visit ALL the callback.
        var allCallBacks = [];

        var deviceErrorCB = function (event) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);
            var error = new CloverError(CloverError.DEVICE_ERROR, event);
            txnRequestCallback(error, null);
        };
        this.device.on(WebSocketDevice.DEVICE_ERROR, deviceErrorCB);
        allCallBacks.push({"event": WebSocketDevice.DEVICE_ERROR, "callback": deviceErrorCB});

        var generalErrorCB = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);
            var error = new CloverError(CloverError.ERROR, message);
            txnRequestCallback(error, null);
        };
        this.device.on(LanMethod.ERROR, generalErrorCB);
        allCallBacks.push({"event": LanMethod.ERROR, "callback": generalErrorCB});

        var connectionErrorCB = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);
            var error = new CloverError(CloverError.COMMUNICATION_ERROR, message);
            txnRequestCallback(error, null);
        };
        this.device.on(WebSocketDevice.CONNECTION_ERROR, connectionErrorCB);
        allCallBacks.push({"event": WebSocketDevice.CONNECTION_ERROR, "callback": connectionErrorCB});

        //Wire in the handler for completion to be called once.
        /**
         * Wire in automatic signature verification for now
         */
        var verifySignatureCB = function (message) {
            try {
                var payload = JSON.parse(message.payload);
                var payment = JSON.parse(payload.payment);
                // Already an object...hmmm
                signature = payload.signature;
                // This has the potential to 'stall out' the
                // sale processing if the user of the API does not register
                // a callback for this message, and verify the signature themselves.
                if (autoVerifySignature) {
                    me.device.sendSignatureVerified(payment);
                }
            } catch (error) {
                var cloverError = new CloverError(LanMethod.VERIFY_SIGNATURE,
                    "Failure attempting to send signature verification", error);
                txnRequestCallback(cloverError, {
                    "code": "ERROR",
                    "signature": signature,
                    "request": txnInfo
                });
            }
        };
        this.device.once(LanMethod.VERIFY_SIGNATURE, verifySignatureCB);
        allCallBacks.push({"event": LanMethod.VERIFY_SIGNATURE, "callback": verifySignatureCB});

        var finishOKCB = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);

            var payload = JSON.parse(message.payload);
            var txnInfo = JSON.parse(payload[txnName]);//payment, credit
            var callbackPayload = {};
            callbackPayload.request = payIntent;
            callbackPayload[txnName] = txnInfo;
            callbackPayload.signature = signature;
            callbackPayload.code = txnInfo.result;

            try {
                txnRequestCallback(null, callbackPayload);
            } catch (err) {
                me.log.error(err);
            }
            me.endOfOperation();
        };
        this.device.once(LanMethod.FINISH_OK, finishOKCB);
        allCallBacks.push({"event": LanMethod.FINISH_OK, "callback": finishOKCB});

        var finishCancelCB = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);

            var callbackPayload = {};
            callbackPayload.request = payIntent;
            callbackPayload.signature = signature;
            callbackPayload.code = "CANCEL";

            var error = new CloverError(CloverError.CANCELED, "Transaction canceled");
            try {
                txnRequestCallback(error, callbackPayload);
            } catch (err) {
                me.log.error(err);
            }
            me.endOfOperation();
        };
        this.device.once(LanMethod.FINISH_CANCEL, finishCancelCB);
        allCallBacks.push({"event": LanMethod.FINISH_CANCEL, "callback": finishCancelCB});

        try {
            this.device.sendTXStart(payIntent, suppressOnScreenTips);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.TX_START,
                "Failure attempting to send start transaction", error);
            txnRequestCallback(cloverError, {
                "code": "ERROR",
                "request": txnInfo
            });
        }
    };

    /**
     *
     * @private
     *
     * @param callbackPayload - the initial callback payload.  This is an object that will
     *  eventually be passed as data to the completionCallback.  This will add a "sent"
     *  property to this object
     * @param {requestCallback} completionCallback - called with an ACK message.  Payload is the
     *  callbackPayload with a boolean "sent" property added.
     * @param returnToWelcomeScreen
     *  @return the unique identifier that should be used when sending the message for which a ACK
     *      message is desired.
     */
    this.genericAcknowledgedCall = function (callbackPayload, completionCallback, returnToWelcomeScreen) {
        // Reserve a reference to this object
        var me = this;
        // We will generate a uuid to use in a callback
        var uuid = null;
        // Add ACK callback

        // Define the callback function just for this.  We need to define it this way
        // because we want a reference to remove it inside itself.
        var genericAckCallback = function (message) {
            // We are looking for a specific ACK callback
            if (message.id == uuid) {
                // Remove the callback as soon as possible. We only want to be
                // called once.
                me.device.removeListener(LanMethod.ACK, genericAckCallback);
                // Build up a callback data object
                if ((typeof callbackPayload == 'undefined') || (callbackPayload == null)) {
                    callbackPayload = {};
                }
                // Add in the value for successfully sent.
                // This is redundant right now, but perhaps not forever
                callbackPayload.result = {
                    "messageReceived": true
                };
                // Call the callback with the data
                try {
                    if (completionCallback) completionCallback(null, callbackPayload);
                    // Show the welcome screen after the acknowledgement.
                    // This might be removed later
                } catch (err) {
                    me.log.error(err);
                }
                if(returnToWelcomeScreen) {
                    me.device.sendShowWelcomeScreen();
                }
            }
        };
        // Generate the uuid so we can filter properly
        uuid = CloverID.guid();
        // Register the callback.
        this.device.on(LanMethod.ACK, genericAckCallback);
        // return the uuid so the caller of this function can use
        // it when sending a message.
        return uuid;
    };

    /**
     *
     * @param {Payment} payment - the payment information returned from a call to 'sale'.
     *  this can be truncated to be only { "id": paymentId, "order": {"id": orderId}}
     * @param {VoidReason} voidReason - the reason for the void.
     * @param {requestCallback} completionCallback
     */
    this.voidTransaction = function (payment, voidReason, completionCallback) {
        var callbackPayload = {"request": {"payment": payment, "voidReason": voidReason}};

        // Temporary - Will be replaced by employee on backend
        if (!payment.hasOwnProperty("employee")) {
            payment["employee"] = {
                "id": "DFLTEMPLOYEE"
            }
        }

        var uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback, true);
        try {
            this.device.sendVoidPayment(payment, voidReason, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.VOID_PAYMENT,
                "Failure attempting to send void", error);
            completionCallback(cloverError, {
                "code": "ERROR",
                "request": callbackPayload
            });
        }
    };

    /**
     * Refund from a previous payment.
     * @param {RefundRequest} refundRequest - the refund request
     * @param {requestCallback} completionCallback
     */
    this.refundPayment = function (refundRequest, completionCallback) {
        var callbackPayload = {"request": refundRequest, "response": {}};
        var txnName = 'refund';

        var me = this;
        // Holds all the callbacks so that they can be removed later, if
        // They need to be.  Callbacks need to be removed in the 'end states'
        // that do not visit ALL the callback.
        var allCallBacks = [];

        // TODO:  Remove in future version, CLOVER-17724
        // This information will be in the finish_* messages returned.
        var refundResponseCB = function (message) {
            if (completionCallback) {
                var payload = JSON.parse(message.payload);

                callbackPayload.response.message = payload['message'];
                callbackPayload.response.reason = payload['reason'];
            }
        };
        this.device.once(LanMethod.REFUND_RESPONSE, refundResponseCB);
        allCallBacks.push({"event": LanMethod.REFUND_RESPONSE, "callback": refundResponseCB});

        var finishOKCB = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);
            try {
                if (completionCallback) {
                    var payload = JSON.parse(message.payload);
                    var txnInfo = JSON.parse(payload[txnName]);//refund
                    callbackPayload.response[txnName] = txnInfo;
                    callbackPayload.response.code = "OK"; // finish ok

                    completionCallback(null, callbackPayload);
                }
            } catch (err) {
                me.log.error(err);
            }
          me.endOfOperation();
        };
        this.device.once(LanMethod.FINISH_OK, finishOKCB);
        allCallBacks.push({"event": LanMethod.FINISH_OK, "callback": finishOKCB});

        var finishCancelCB = function (message) {
            // Remove obsolete listeners.  This is an end state
            me.device.removeListeners(allCallBacks);
            try {
                if (completionCallback) {
                    callbackPayload.response.code = "CANCEL";

                    var error = new CloverError(CloverError.CANCELED, "Transaction canceled");
                    completionCallback(error, callbackPayload);
                }
            } catch (err) {
                me.log.error(err);
            }
          me.endOfOperation();
        };
        this.device.once(LanMethod.FINISH_CANCEL, finishCancelCB);
        allCallBacks.push({"event": LanMethod.FINISH_CANCEL, "callback": finishCancelCB});

        try {
          if (refundRequest["version"] && refundRequest["version"] === 2) {
            this.device.sendRefundV2(refundRequest.orderId, refundRequest.paymentId,
              refundRequest["amount"], refundRequest["fullRefund"]);
          } else {
            this.device.sendRefund(refundRequest.orderId, refundRequest.paymentId, refundRequest["amount"]);
          }
        } catch (error) {
            var cloverError = new CloverError(LanMethod.REFUND_REQUEST,
                "Failure attempting to send refund request", error);
            callbackPayload["code"] = "ERROR";
            completionCallback(cloverError, callbackPayload);
        }
    };

    /**
     * Print an array of strings on the device
     *
     * @param {string[]} textLines - an array of strings to print
     * @param {requestCallback} [completionCallback]
     */
    this.print = function (textLines, completionCallback) {
        var callbackPayload = {"request": textLines};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback, true);
        }
        try {
            this.device.sendPrintText(textLines, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.PRINT_TEXT,
                "Failure attempting to print text", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
        }
    };

    /**
     * Print a receipt from a previous transaction.
     * @param printRequest
     * @param completionCallback
     */
    this.printReceipt = function (printRequest, completionCallback) {
        var callbackPayload = {"request": printRequest};
        var me = this;
        var allCallBacks = [];

        var onUiState = function onUiState(message) {
            var payload = JSON.parse(message.payload);
            if(payload.uiState == "RECEIPT_OPTIONS") {
                if(payload.uiDirection == "ENTER") {
                    // we can ignore this.  If a POS wants to do more
                    // granular integration, they will need to write their
                    // own handler.
                } else if(payload.uiDirection == "EXIT") {
                    if(payload["uiState"] == "RECEIPT_OPTIONS") {
                        // Remove the UI callback handler, we are done with it.
                        me.device.removeListeners(allCallBacks);
                        me.endOfOperation();
                    }
                } else {
                    console.log("Unknown ui event direction:" + payload.uiDirection);
                }
            }
        };
        // Listen for UI_STATE messages
        clover.device.on(LanMethod.UI_STATE, onUiState);
        allCallBacks.push({"event": LanMethod.UI_STATE, "callback": onUiState});

        try {
            this.device.sendShowPaymentReceiptOptionsV2(printRequest.orderId, printRequest.paymentId);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.SHOW_PAYMENT_RECEIPT_OPTIONS,
                "Failure attempting to print receipt", error);
            completionCallback(cloverError, {
                "code": "ERROR",
                "request": callbackPayload
            });
        }
    };

    /**
     * Prints an image on the receipt printer of the device.
     *
     * The size of the image should be limited, and the optimal
     * width of the image is 384 pixals.
     *
     * @param img an HTML DOM IMG object.
     * @param {requestCallback} [completionCallback]
     */
    this.printImage = function (img, completionCallback) {
        var callbackPayload = {"request": {"img": {"src": img.src}}};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback, true);
        }
        try {
            this.device.sendPrintImage(img, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.PRINT_IMAGE,
                "Failure attempting to print image", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
        }
    };

    /**
     * Prints an image on the receipt printer of the device.
     *
     * The size of the image should be limited, and the optimal
     * width of the image is 384 pixals.
     *
     * @param img an HTML DOM IMG object.
     * @param {requestCallback} [completionCallback]
     */
    this.printImageFromURL = function (img, completionCallback) {
        var callbackPayload = {"request":{"img":{"url": img }}};
        var uuid = null;
        if(completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback, true);
        }
        try {
            this.device.sendPrintImageFromURL(img, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.PRINT_IMAGE,
                "Failure attempting to print image", error);
            if(completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
        }
    };

    /**
     * Sends an escape code to the device.  The behavior of the device when this is called is
     * dependant on the current state of the device.
     * @param {requestCallback} [completionCallback]
     */
    this.sendCancel = function (completionCallback) {
        // Note - this is a pattern for sending keystrokes ot the device.
        // Available keystrokes can be found in KeyPress.
        var callbackPayload = {"request": "cancel"};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback, true);
        }
        try {
            this.device.sendKeyPress(KeyPress.ESC, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.KEY_PRESS,
                "Failure attempting to cancel", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * Sends a break message to the device to reset
     * the state of the device.
     * <B>Warning: this will cause any pending transaction messages to be lost!</B>
     * @param {requestCallback} [completionCallback]
     */
    this.resetDevice = function(completionCallback) {
      // Note - this is a pattern for sending keystrokes ot the device.
      // Available keystrokes can be found in KeyPress.
      var callbackPayload = {"request": "break"};
      var uuid = null;
      if (completionCallback) {
          uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback, true);
      }
      try {
          this.device.sendBreak(uuid);
      } catch (error) {
          var cloverError = new CloverError(LanMethod.BREAK,
              "Failure attempting to reset device", error);
          if (completionCallback) {
              completionCallback(cloverError, {
                  "code": "ERROR",
                  "request": callbackPayload
              });
          }
          console.log(cloverError);
      }
    };

    /**
     * Opens the cash drawer
     *
     * @param {string} reason - the reason the cash drawer was opened.
     * @param {requestCallback} [completionCallback]
     */
    this.openCashDrawer = function (reason, completionCallback) {
        // Note - this is a pattern for sending keystrokes ot the device.
        // Available keystrokes can be found in KeyPress.
        var callbackPayload = {"request": {"reason": reason}};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback, true);
        }
        try {
            this.device.sendOpenCashDrawer(reason, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.OPEN_CASH_DRAWER,
                "Failure attempting to open the cash drawer", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * @param {TipAdjustRequest} tipAdjustRequest - the refund request
     * @param {requestCallback} completionCallback
     */
    this.tipAdjust = function (tipAdjustRequest, completionCallback) {
        var callbackPayload = {"request": tipAdjustRequest};

        var tipAdjustCB = function (message) {
            var payload = JSON.parse(message.payload);
            callbackPayload["response"] = payload;
            callbackPayload["code"] = "SUCCESS";

            completionCallback(null, callbackPayload);
        }.bind(this);
        this.device.once(LanMethod.TIP_ADJUST_RESPONSE, tipAdjustCB);

        try {
            this.device.sendTipAdjust(tipAdjustRequest.orderId, tipAdjustRequest.paymentId, tipAdjustRequest["tipAmount"]);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.TIP_ADJUST,
                "Failure attempting to send tip adjust", error);
            callbackPayload["code"] = "ERROR";
            completionCallback(cloverError, callbackPayload);
        }
    };

    /**
     * Retreive the last transactional message made to the device.
     *
     * @param {requestCallback} completionCallback
     */
    this.getLastMessage = function (completionCallback) {
        var callbackPayload = {"request": {}};

        var lastMessageCB = function (message) {
            var payload = JSON.parse(message.payload);
            callbackPayload["response"] = payload;

            completionCallback(null, callbackPayload);
        }.bind(this);
        this.device.once(LanMethod.LAST_MSG_RESPONSE, lastMessageCB);

        try {
            this.device.sendLastMessageRequest();
        } catch (error) {
            var cloverError = new CloverError(LanMethod.LAST_MSG_REQUEST,
                "Failure attempting to get last message sent", error);
            completionCallback(cloverError, {
                "code": "ERROR",
                "request": callbackPayload
            });
        }
    };

    /**
     * Capture a card for future usage in a sale or auth request.
     *
     * @param cardEntryMethods
     * @param {requestCallback} completionCallback
     */
    this.vaultCard = function (cardEntryMethods, completionCallback) {
        var callbackPayload = {"request": {"cardEntryMethods": cardEntryMethods}};
        var me = this;

        var vaultCardCB = function (message) {
            var payload = JSON.parse(message.payload);
            callbackPayload["response"] = payload;

            completionCallback(null, callbackPayload);
            me.endOfOperation();
        }.bind(this);
        this.device.once(LanMethod.VAULT_CARD_RESPONSE, vaultCardCB);

        try {
            this.device.sendVaultCard(cardEntryMethods);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.VAULT_CARD,
                "Failure attempting to capture card", error);
            completionCallback(cloverError, {
                "code": "ERROR",
                "request": callbackPayload
            });
        }
    };

    /**
     * Capture a previously made preauthorization.
     *
     * @param {CapturePreAuthRequest} request
     * @param completionCallback
     */
    this.capturePreAuth = function (request, completionCallback) {
        var callbackPayload = {"request": request};

        // Validate request has contents
        if (!request.hasOwnProperty("paymentId")) {
            completionCallback(new CloverError(CloverError.INVALID_DATA, "missing paymentId"), null);
        }
        if (!request.hasOwnProperty("amount")) {
            completionCallback(new CloverError(CloverError.INVALID_DATA, "missing amount"), null);
        }

        var capturePreAuthCB = function (message) {
            var payload = JSON.parse(message.payload);
            callbackPayload["response"] = payload;
            completionCallback(null, callbackPayload);
        }.bind(this);
        this.device.once(LanMethod.CAPTURE_PREAUTH_RESPONSE, capturePreAuthCB);

        try {
            this.device.sendCapturePreAuth(request.orderId, request.paymentId, request.amount, request["tipAmount"]);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.LAST_MSG_REQUEST,
                "Failure attempting to get last message sent", error);
            completionCallback(cloverError, {
                "code": "ERROR",
                "request": callbackPayload
            });
        }
    };

    /**
     *
     * Send a closeout request to the device.
     *
     * @param {CloseoutRequest} request
     * @param completionCallback
     */
    this.closeout = function (request, completionCallback) {
        if (!request)request = {};
        var callbackPayload = {"request": request};

        // Validate request has contents
        if (request.hasOwnProperty("allowOpenTabs")) {
            if (!typeof(request.allowOpenTabs) === "boolean")
                completionCallback(new CloverError(CloverError.INVALID_DATA, "allowOpenTabs must be true or false"), null);
        }

        var closeoutCB = function (message) {
            var payload = JSON.parse(message.payload);
            callbackPayload["response"] = payload;
            completionCallback(null, callbackPayload);
        }.bind(this);
        this.device.once(LanMethod.CLOSEOUT_RESPONSE, closeoutCB);

        try {
            // 'batchId' is left undocumented for now, and not supported.  Only
            // Current open batches will be closed out.
            this.device.sendCloseout(request["allowOpenTabs"], request["batchId"]);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.CLOSEOUT_RESPONSE,
                "Failure attempting to send closeout request", error);
            completionCallback(cloverError, {
                "code": "ERROR",
                "request": callbackPayload
            });
        }
    };

    /**
     * Puts a message on the device screen
     *
     * @param {string} message - the message to put on the device screen.
     * @param {requestCallback} [completionCallback]
     */
    this.showMessage = function (message, completionCallback) {
        // Note - this is a pattern for sending keystrokes ot the device.
        // Available keystrokes can be found in KeyPress.
        var callbackPayload = {"request": {"message": message}};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback);
        }
        try {
            this.device.sendTerminalMessage(message, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.TERMINAL_MESSAGE,
                "Failure attempting to put message on device", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * Puts the device in the welcome screen
     *
     * @param {requestCallback} [completionCallback]
     */
    this.showWelcomeScreen = function (completionCallback) {
        // Note - this is a pattern for sending keystrokes ot the device.
        // Available keystrokes can be found in KeyPress.
        var callbackPayload = {"request": {}};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback);
        }
        try {
            this.device.sendShowWelcomeScreen(uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.SHOW_WELCOME_SCREEN,
                "Failure attempting to put message on device", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * Puts the device in the 'thank you' screen
     *
     * @param {requestCallback} [completionCallback]
     */
    this.showThankYouScreen = function (completionCallback) {
        // Note - this is a pattern for sending keystrokes ot the device.
        // Available keystrokes can be found in KeyPress.
        var callbackPayload = {"request": {}};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback);
        }
        try {
            this.device.sendShowThankYouScreen(uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.SHOW_THANK_YOU_SCREEN,
                "Failure attempting to put message on device", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * Sends a message to accept the passed signature on the payment.
     *
     * @param {SignatureVerifyRequest} signatureVerifyRequest - the signature verification request.
     * @param completionCallback
     */
    this.acceptSignature = function (signatureVerifyRequest, completionCallback) {
        // Get the payment from the raw message
        var payment = JSON.parse(signatureVerifyRequest.payment);

        // For reference, this is how you can get the signature from
        // the payload of the message
        // signature = signatureVerifyRequest.signature;

        var callbackPayload = {"request": signatureVerifyRequest};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback);
        }
        try {
            this.device.sendSignatureVerified(payment, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.SIGNATURE_VERIFIED,
                "Failure attempting to verify signature", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * Sends a message to reject the passed signature on the payment.
     *
     * @param {SignatureVerifyRequest} signatureVerifyRequest - the signature verification request.
     * @param completionCallback
     */
    this.rejectSignature = function (signatureVerifyRequest, completionCallback) {
        // Get the payment from the raw message
        var payment = JSON.parse(signatureVerifyRequest.payment);

        // For reference, this is how you can get the signature from
        // the payload of the message
        // signature = signatureVerifyRequest.signature;

        var callbackPayload = {"request": signatureVerifyRequest};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback);
        }
        try {
            this.device.sendSignatureRejected(payment, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.SIGNATURE_VERIFIED,
                "Failure attempting to reject signature", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * Display the passed order.
     *
     * @param {DisplayOrder} order
     * @param completionCallback
     */
    this.displayOrder = function (order, completionCallback) {
        var callbackPayload = {"request": order};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback);
        }
        try {
            this.device.sendShowOrderScreen(order, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.SHOW_ORDER_SCREEN,
                "Failure attempting to display order", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    // Expects that the lineitem has already been set on the order...
    // this is consistent with the windows api.
    /**
     * Tells the device to redisplay the passed order, and that a lineItem
     * has been added to it.  The lineItem should already be incorperated
     * to the order before calling this function.
     *
     * @param {DisplayOrder} order - the order to display
     * @param {DisplayLineItem} lineItem - the line item that has been added to the order
     * @param completionCallback
     */
    this.displayOrderLineItemAdded = function (order, lineItem, completionCallback) {
        this.displayOrderInternal(order, "lineItem", lineItem,
            this.device.sendShowOrderLineItemAdded.bind(this.device),
            completionCallback);
    };

    /**
     * Tells the device to redisplay the passed order, and that a lineItem
     * has been removed from it.  The lineItem should already be disincorperated
     * from the order before calling this function.
     *
     * @param {DisplayOrder} order - the order to display
     * @param {DisplayLineItem} lineItem - the line item that has been removed to the order
     * @param completionCallback
     */
    this.displayOrderLineItemRemoved = function (order, lineItem, completionCallback) {
        this.displayOrderInternal(order, "lineItem", lineItem,
            this.device.sendShowOrderLineItemRemoved.bind(this.device),
            completionCallback);
    };

    /**
     * Tells the device to redisplay the passed order, and that a discount
     * has been added to it.  The discount should already be incorperated
     * to the order before calling this function.
     *
     * @param {DisplayOrder} order - the order to display
     * @param {DisplayDiscount} discount - the discount that has been added to the order
     * @param completionCallback
     */
    this.displayOrderDiscountAdded = function (order, discount, completionCallback) {
        this.displayOrderInternal(order, "discount", discount,
            this.device.sendShowOrderDiscountAdded.bind(this.device),
            completionCallback);
    };

    /**
     * Tells the device to redisplay the passed order, and that a discount
     * has been removed from it.  The discount should already be disincorperated
     * from the order before calling this function.
     *
     * @param {DisplayOrder} order - the order to display
     * @param {DisplayDiscount} discount - the discount that has been removed from the order
     * @param completionCallback
     */
    this.displayOrderDiscountRemoved = function (order, discount, completionCallback) {
        this.displayOrderInternal(order, "discount", discount,
            this.device.sendShowOrderDiscountRemoved.bind(this.device),
            completionCallback);
    };

    /**
     * Does common functionality to display modified orders
     *
     * @private
     * @param order
     * @param orderComponentName
     * @param orderComponent
     * @param deviceFunction
     * @param completionCallback
     */
    this.displayOrderInternal = function (order,
                                      orderComponentName,
                                      orderComponent,
                                      deviceFunction,
                                      completionCallback) {
        var callbackPayload = {"request": {"order": order, orderComponentName: orderComponent}};
        var uuid = null;
        if (completionCallback) {
            uuid = this.genericAcknowledgedCall(callbackPayload, completionCallback);
        }
        try {
            var cloverError = null;
            if (order == null) {
                cloverError = new CloverError(CloverError.INVALID_DATA,
                    "DisplayOrder object cannot be null. ");
            }
            if (order.id == null) {
                cloverError = new CloverError(CloverError.INVALID_DATA,
                    "DisplayOrder id cannot be null. " + order);
            }
            if (orderComponent == null) {
                cloverError = new CloverError(CloverError.INVALID_DATA,
                    orderComponentName + " cannot be null. ");
            }
            if (cloverError) {
                if (completionCallback) {
                    completionCallback(cloverError, {
                        "code": "ERROR",
                        "request": callbackPayload
                    });
                }
                return;
            }
            // The operation
            var operation = {};
            operation.orderId = order.id;
            operation.ids = {};
            operation.ids.elements = [];
            operation.ids.elements.push(orderComponent.id);

            // this.device.sendShowOrderDiscountRemoved(order);
            deviceFunction(order, operation, uuid);
        } catch (error) {
            var cloverError = new CloverError(LanMethod.SHOW_ORDER_SCREEN,
                "Failure attempting to display order", error);
            if (completionCallback) {
                completionCallback(cloverError, {
                    "code": "ERROR",
                    "request": callbackPayload
                });
            }
            this.log.error(cloverError);
        }
    };

    /**
     * action after an operation
     * @private
     */
    this.endOfOperation = function() {
        // Say "Thank you" for three seconds
        this.device.sendShowThankYouScreen();
        // Then say "Welcome"
        setTimeout(
          function () {
              this.device.sendShowWelcomeScreen();
          }.bind(this), 3000 // three seconds
        );
    };

    //////////

    ///**
    // * Not yet implemented
    // */
    //this.getSiteTotals = function() {
    //    throw new Error("Not yet implemented");
    //}
    //
    ///**
    // * Not yet implemented
    // */
    //this.rebootDevice = function() {
    //    throw new Error("Not yet implemented");
    //}
    //
    ///**
    // * Not yet implemented
    // */
    //this.balance = function() {
    //    throw new Error("Not yet implemented");
    //}
    //
    ///**
    // * Not yet implemented
    // */
    //this.getCardData = function() {
    //    throw new Error("Not yet implemented");
    //}
}

// Below are utility methods that can be located anywhere.
// Want to make sure we have the namespace, so putting in Clover.
/**
 * @private
 * @param value
 * @returns {boolean}
 */
Clover.isInt = function(value) {
    var x;
    if (isNaN(value)) {
        return false;
    }
    x = parseFloat(value);
    return (x | 0) === x;
};

Clover.minimalConfigurationPossibilities = [
    ["deviceURL"],
    ["clientId", "domain", "deviceSerialId"],
    ["clientId", "domain", "deviceId"]
];

//
// Expose the module.
//
if ('undefined' !== typeof module) {
    module.exports = Clover;
}

/**
 * An object used to control an order display on the device.
 *
 * @typedef {Object} DisplayOrder
 * @property {string} id - identifier for the order
 * @property {DisplayDiscountArray} discounts
 * @property {DisplayLineItemArray} lineItems
 * @property {string} tax - the formatted tax amount to display
 * @property {string} subtotal - the formatted subtotal amount to display
 * @property {string} total - the formatted total (subtotal+tax) to display
 */

/**
 * A container object used when serializing a collection of display discount items
 *
 * @typedef {Object} DisplayDiscountArray
 * @property {DisplayDiscount[]} elements - the array of items
 */

/**
 * A discount applied to a line item on the order.
 *
 * @typedef {Object} DisplayDiscount
 * @property {string} id - identifier for the discount
 * @property {string} lineItemId - identifier for the line item this applies to
 * @property {string} name - name of the discount
 * @property {string} amount - amount of the discount
 * @property {string} percentage - percentage of the discount
 */

/**
 * A container object used when serializing a collection of display line items
 *
 * @typedef {Object} DisplayLineItemArray
 * @property {DisplayLineItem[]} elements - the array of items
 */

/**
 *
 * @typedef {Object} DisplayLineItem
 * @property {string} id - identifier for the discount
 * @property {string} orderId - identifier for the order this applies to
 * @property {string} name - name of the discount
 * @property {string} price - the price of the item
 * @property {string} quantity - number of items
 * @property {DisplayDiscountArray} discounts - number of items
 * @property {string} discountAmount - the formatted discount total to display
 */

/**
 * @typedef {Object} SignatureVerifyRequest
 * @property {string} payment - the payment information, serialized to a sting
 * @property {Signature} signature - the signature
 */

/**
 * This callback type is called `requestCallback` and is displayed as a global symbol.  This type
 * of callback adheres to the Node.js convention of 'Error-first' callbacks.
 *
 *
 * The first argument of the callback is always reserved for an error object.
 * On a successful response, the ‘err’ argument is null. Call the callback and include the successful data only.
 *
 * On an unsuccessful response, the ‘err’ argument is set. Call the callback with an actual error object. The
 * error should describe what happened and include enough information to tell the callback what went wrong. Data
 * can still be returned in the other arguments as well, but generally the error is passed alone.
 * @see http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/
 *
 * @callback requestCallback
 * @param {Error} [error] - null iff there was no error, else an object that contains a code and a message text
 * @param {Object} [result] - data that results from the function.  This is a hash of information.  There will be a
 *  code within the object that indicates the operation completion status.  This may be null, depending on the
 *  nature of the call, and the error state.
 */

/**
 * A payment
 *
 * @typedef {Object} TransactionRequest
 * @property {Number} amount - the amount of a sale or refund, including tax.  Must be an integer
 * @property {Number} [tipAmount] - the amount of a tip.  Added to the amount for the total.  Valid for sale operations.
 *  Must be an integer
 * @property {Number} [tippableAmount] - the amount that calculated tips are based on.  If not set then 'amount'
 *  is used. Valid for sale operations.  Must be an integer
 * @property {string} [employeeId] - the valid Clover id of an employee recognized by the device.  Represents the
 *  employee making this sale or refund.
 * @property {boolean} [autoVerifySignature] - optional override to allow either automatic signature verification
 *  {true}, or expect that the caller has registered a listener for the request for signature verification {false}.
 *  This will override the internal object flag autoVerifySignature.
 * @property {string} [requestId] - optional CloverID compatible identifier used for the payment once the transaction
 *  is completed.  See @CloverID
 * @property {boolean} [isPreAuth] - valid for auth calls.  If set, the payment will be made as a pre authorization.
 */

/**
 * The response to a sale or naked refund
 *
 * @typedef {Object} TransactionResponse
 * @property {string} code - the result code for the transaction - "SUCCESS", "CANCEL", "ERROR"
 * @property {Payment} payment - the payment information
 * @property {Credit} credit - the payment information
 * @property {Signature} [signature] - the signature, if present
 * @property {TransactionRequest} [request] - the request that resulted in this response
 */

/**
 * The callback on a sale
 *
 * @callback Clover~transactionRequestCallback
 * @param {Error} [error] - null iff there was no error, else an object that contains a code and a message text
 * @param {TransactionResponse} result - data that results from the function.
 */

/**
 * @typedef {Object} CloseoutRequest
 * @property {boolean} [allowOpenTabs] - if true, indicates that a closeout can proceed, even if open tabs are
 *  present.  Defaults to false.
 */

/**
 * @typedef {Object} RefundRequest
 * @property {string} orderId - the id of the order to refund
 * @property {string} paymentId - the id of the payment on the order to refund
 * @property {boolean} [fullRefund] - if true, then a full refund is done for the version 2 call.
 * @property {number} [amount] - the amount to refund.  If not included or 0, version 1 will refund the full payment.
 *  If using the version 2 call, 0 will result in an error. The amount cannot exceed the original payment, and
 *  additional constraints apply to this (EX: if a partial refund has already been performed then the amount
 *  cannot exceed the remaining payment amount).
 * @property {Number} [version] - the version of the refund request.  If not included, the current version of the
 *  call is used.  Must be an integer
 */

/**
 * @typedef {Object} CapturePreAuthRequest
 * @property {string} paymentId - the id of the payment to capture
 * @property {number} amount - the final amount ofthe payment less any included tip
 * @property {number} [tipAmount] - the amount of the tip.
 */

/**
 * @typedef {Object} TipAdjustRequest
 * @property {string} orderId - the id of the order to adjust
 * @property {string} paymentId - the id of the payment on the order to adjust
 * @property {number} tipAmount - the amount to adjust.
 */

/**
 * @typedef {Object} Payment
 * @property {string} result - the result code for the transaction - "SUCCESS", "CANCEL"
 * @property {Number} [createdTime] - the time in milliseconds that the transaction successfully completed.
 *  Must be an integer
 * @property {CardTransaction} [cardTransaction] - successful transaction information
 * @property {Number} [amount] - the amount of the transaction, including tax.  Must be an integer
 * @property {Number} [tipAmount] - added tip amount.  Must be an integer
 * @property {Object} [order] - order information. Ex: id - the order id
 * @property {Object} [employee] - employee information. Ex: id - the employee id
 */

/**
 * @typedef {Object} Credit
 * @property {Number} [amount] - the amount of the transaction, including tax.  Must be an integer
 * @property {Number} [createdTime] - the time in milliseconds that the transaction successfully completed.  Must be
 *  an integer
 * @property {Tender} [tender] - refund information
 * @property {Object} [orderRef] - order information. Ex: id - the order id
 * @property {Object} [employee] - employee information. Ex: id - the employee id
 * @property {CardTransaction} [cardTransaction] - successful transaction information
 */

/**
 * @typedef {Object} Tender
 * @property {string} id - the tender id
 * @property {string} label - the label that describes the tender
 */

/**
 * @typedef {Object} CardTransaction
 * @property {number} authcode - the authorization code.  Must be an integer
 * @property {string} entryType - SWIPED, KEYED, VOICE, VAULTED, OFFLINE_SWIPED, OFFLINE_KEYED, EMV_CONTACT,
 *  EMV_CONTACTLESS, MSD_CONTACTLESS, PINPAD_MANUAL_ENTRY
 * @property {Object} extra - additional information on the transaction.  EX: cvmResult - "SIGNATURE"
 * @property {string} state - PENDING, CLOSED
 * @property {string} referenceId
 * @property {string} type - AUTH, PREAUTH, PREAUTHCAPTURE, ADJUST, VOID, VOIDRETURN, RETURN, REFUND,
 *  NAKEDREFUND, GETBALANCE, BATCHCLOSE, ACTIVATE, BALANCE_LOCK, LOAD, CASHOUT, CASHOUT_ACTIVE_STATUS,
 *  REDEMPTION, REDEMPTION_UNLOCK, RELOAD
 * @property {Number} transactionNo.  Must be an integer
 * @property {Number} last4 - the last 4 digits of the card.  Must be an integer
 * @property {string} cardType - VISA, MC, AMEX, DISCOVER, DINERS_CLUB, JCB, MAESTRO, SOLO, LASER,
 *  CHINA_UNION_PAY, CARTE_BLANCHE, UNKNOWN, GIFT_CARD, EBT
 *
 */

/**
 * @typedef {Object} Signature
 * @property {Stroke[]} strokes - the strokes of the signature.  A series of points representing a single contiguous
 *  line
 * @property {Number} height - the pixal height of the canvas area needed to correctly represent the signature.
 *  Must be an integer
 * @property {Number} width - the pixal width of the canvas area needed to correctly represent the signature.
 *  Must be an integer
 *
 */

/**
 * @typedef {Object} Stroke - A series of points representing a single contiguous line
 * @property {Point[]} points
 */

/**
 * @typedef {Object} Point
 * @property {Number} x - the x coordinate location of the point in pixals.  Must be an integer
 * @property {Number} y - the y coordinate location of the point in pixals.  Must be an integer
 */


/**
 * The configuration for the Clover api object.  This encapsulates the different ways that the Clover object can
 * be configured for proper access to the device.
 *
 * <p>
 *     Possible configurations:<br/>
 *     <ol>
 *          <li>deviceURL</li>
 *          <li>oauthToken, domain, merchantId, deviceSerialId</li>
 *          <li>clientId, domain, merchantId, deviceId (Requires log in to Clover server)</li>
 *          <li>clientId, domain, merchantId, deviceSerialId (Requires log in to Clover server)</li>
 *     </ol>
 * </p>
 *
 * @typedef {Object} CloverConfig
 * @property {string} [deviceURL] - the web socket url to use when connecting to the device.  Optional
 *  if other configuration values allow this to be obtained.
 * @property {string} [oauthToken] - the authentication token used when communicating with the clover cos
 *  server.  Required if deviceURL is not set. Optional if other configuration values allow this
 *  to be obtained.
 * @property {string} [domain] - the url to the clover cos server.
 * @property {string} [merchantId] - the merchant id.
 * @property {string} [deviceSerialId] - the serial id of the device to use.
 * @property {string} [clientId] - the Clover application id to use when obtaining the oauth token.
 * @property {boolean} [autoVerifySignature] - if set to false, a callback must be registered for
 *  signature verification requests.  This defaults to true.
 * @property {boolean} [disableRestartTransactionWhenFailed] - if set to true, when the device times out
 *  during a transaction, it will return to the 'Welcome' screen when the customer selects 'ok'
 * @property {boolean} [remotePrint] - if set to true, then when the user selects "print" on the print receipt
 *  screen after a transaction, a PRINT_PAYMENT message will be sent from the device to the API.  To get the
 *  message, a listener must be registered via Clover.device.on(LanMethod.PRINT_PAYMENT, ...)
 */