UNPKG

ut-codec

Version:
646 lines (625 loc) 29.2 kB
var merge = require('lodash.merge'); var map = require('./ndcMap'); var defaultFormat = require('./ndc.messages'); var hrtime = require('browser-process-hrtime'); var emv = require('./emv'); function packCamFlags(data) { return data.reduce((buf, byte, idx) => { buf[idx] = byte.reduce((b, bit, idx) => { return (b << 1) | bit; }, buf[idx]); return buf; }, new Buffer([0, 0])).toString('hex'); } function packSmartCardData(camFlags, emvTags) { let result = '5CAM'; if (camFlags) { result += packCamFlags(camFlags); } if (emvTags) { result += emv.tagsEncode(emvTags); } return result; } function NDC(config, validator, logger) { this.fieldSeparator = config.fieldSeparator || '\u001c'; this.groupSeparator = config.groupSeparator || '\u001d'; this.val = validator || null; this.log = logger || {}; this.codes = {}; this.init(config); return this; } NDC.prototype.init = function(config) { this.messageFormat = merge({}, defaultFormat, config.messageFormat); Object.keys(this.messageFormat).forEach((name) => { var mf = this.messageFormat[name]; mf.fieldsSplit = mf.fields.split(','); mf.method = name; var code = (mf.values.messageClass || '') + (mf.values.messageSubclass || '') + '|' + (mf.values.commandCode || '') + (mf.values.commandModifier || ''); this.codes[code] = mf; }); }; var parsers = { // solicited descriptors transactionReady: (transactionSerialNumber, transactionData) => ({ transactionSerialNumber, transactionData }), specificReject: (status) => { var e = new Error(map.specificErrors[status] || 'Specific command reject'); e.type = 'aptra.commandReject.' + status; throw e; }, reject: () => { var e = new Error('Command reject'); e.type = 'aptra.commandReject'; throw e; }, fault: (deviceIdentifierAndStatus, severities, diagnosticStatus, suppliesStatus) => { var deviceStatus = deviceIdentifierAndStatus && deviceIdentifierAndStatus.substring && deviceIdentifierAndStatus.substring(1); var device = deviceIdentifierAndStatus && deviceIdentifierAndStatus.substring && (map.devices[deviceIdentifierAndStatus.substring(0, 1)] || deviceIdentifierAndStatus.substring(0, 1)); var result = { device, deviceStatus, severities: severities && severities.split && severities.split('').map((severity) => map.severities[severity]), diagnosticStatus, supplies: suppliesStatus && suppliesStatus.split && suppliesStatus.split('').map((status) => map.supplies[status]) }; if (device && device.length > 1 && typeof parsers[device] === 'function') { merge(result, parsers[device](deviceStatus)); } return result; }, ready: () => ({}), state: function(status) { // do not change to arrow function as proper this and arguments are needed var g1 = status.substring(0, 1); var fn = g1 && map.statuses[g1] && this[map.statuses[g1]]; if (typeof fn === 'function') { return merge({ statusType: map.statuses[g1] }, fn.apply(this, arguments)); } }, // terminal state statuses supplyCounters: (counters) => (counters && counters.substring && { transactionSerialNumber: counters.substring(1, 5), transactionCount: Number.parseInt(counters.substring(5, 12), 10), notes1: Number.parseInt(counters.substring(12, 17), 10), notes2: Number.parseInt(counters.substring(17, 22), 10), notes3: Number.parseInt(counters.substring(22, 27), 10), notes4: Number.parseInt(counters.substring(27, 32), 10), session: { cassettes: [ {count: Number.parseInt(counters.substring(12, 17), 10)}, {count: Number.parseInt(counters.substring(17, 22), 10)}, {count: Number.parseInt(counters.substring(22, 27), 10)}, {count: Number.parseInt(counters.substring(27, 32), 10)} ] }, rejected1: Number.parseInt(counters.substring(32, 37), 10), rejected2: Number.parseInt(counters.substring(37, 42), 10), rejected3: Number.parseInt(counters.substring(42, 47), 10), rejected4: Number.parseInt(counters.substring(47, 52), 10), dispensed1: Number.parseInt(counters.substring(52, 57), 10), dispensed2: Number.parseInt(counters.substring(57, 62), 10), dispensed3: Number.parseInt(counters.substring(62, 67), 10), dispensed4: Number.parseInt(counters.substring(67, 72), 10), last1: Number.parseInt(counters.substring(72, 77), 10), last2: Number.parseInt(counters.substring(77, 82), 10), last3: Number.parseInt(counters.substring(82, 87), 10), last4: Number.parseInt(counters.substring(87, 92), 10), captured: Number.parseInt(counters.substring(92, 97), 10) }), datetime: (status) => (status && status.substring && { clockStatus: status.substring(0, 1), datetime: status.substring(1) }), configurationId: (config) => ({ configId: config.substring(1) }), configuration: (config, hwFitness, hwConfig, supplies, sensors, release, softwareId) => { var sensorValues = parsers.sensors(' ' + sensors, true); return {cofigId: config.substring(1), session: { cassettes: [ {sensor: sensorValues.cassette1, fitness: map.severities[hwFitness.substring(15, 16)], supplies: map.suppliesStatus[supplies.substring(15, 16)]}, {sensor: sensorValues.cassette2, fitness: map.severities[hwFitness.substring(16, 17)], supplies: map.suppliesStatus[supplies.substring(16, 17)]}, {sensor: sensorValues.cassette3, fitness: map.severities[hwFitness.substring(17, 18)], supplies: map.suppliesStatus[supplies.substring(17, 18)]}, {sensor: sensorValues.cassette4, fitness: map.severities[hwFitness.substring(18, 19)], supplies: map.suppliesStatus[supplies.substring(18, 19)]} ] }, fitness: hwFitness && hwFitness.substring && { clock: map.severities[hwFitness.substring(0, 1)], comms: map.severities[hwFitness.substring(1, 2)], disk: map.severities[hwFitness.substring(2, 3)], cardReader: map.severities[hwFitness.substring(3, 4)], cashHandler: map.severities[hwFitness.substring(4, 5)], depository: map.severities[hwFitness.substring(5, 6)], receiptPrinter: map.severities[hwFitness.substring(6, 7)], journalPrinter: map.severities[hwFitness.substring(7, 8)], nightDepository: map.severities[hwFitness.substring(10, 11)], encryptor: map.severities[hwFitness.substring(11, 12)], camera: map.severities[hwFitness.substring(12, 13)], doorAccess: map.severities[hwFitness.substring(13, 14)], flexDisk: map.severities[hwFitness.substring(14, 15)], cassette1: map.severities[hwFitness.substring(15, 16)], cassette2: map.severities[hwFitness.substring(16, 17)], cassette3: map.severities[hwFitness.substring(17, 18)], cassette4: map.severities[hwFitness.substring(18, 19)], statementPrinter: map.severities[hwFitness.substring(21, 22)], signageDisplay: map.severities[hwFitness.substring(22, 23)], systemDisplay: map.severities[hwFitness.substring(25, 26)], mediaEntry: map.severities[hwFitness.substring(26, 27)], envelopeDispenser: map.severities[hwFitness.substring(27, 28)], documentProcessing: map.severities[hwFitness.substring(28, 29)], coinDispenser: map.severities[hwFitness.substring(29, 30)], voiceGuidance: map.severities[hwFitness.substring(32, 33)], noteAcceptor: map.severities[hwFitness.substring(34, 35)], chequeProcessor: map.severities[hwFitness.substring(35, 36)] }, hwConfig, supplyStatus: supplies && supplies.substring && { cardReader: map.suppliesStatus[supplies.substring(3, 4)], depository: map.suppliesStatus[supplies.substring(5, 6)], receiptPrinter: map.suppliesStatus[supplies.substring(6, 7)], journalPrinter: map.suppliesStatus[supplies.substring(7, 8)], rejectBin: map.suppliesStatus[supplies.substring(4, 5)], cassette1: map.suppliesStatus[supplies.substring(15, 16)], cassette2: map.suppliesStatus[supplies.substring(16, 17)], cassette3: map.suppliesStatus[supplies.substring(17, 18)], cassette4: map.suppliesStatus[supplies.substring(18, 19)] }, sensors: sensorValues, release, softwareId }; }, hardware: (configuration, product, hardwareConfiguration) => ({ configId: configuration.substring(2), product: product && product.substring && (map.products[product.substring(1)] || ('product' + product.substring(1))), hardwareConfiguration: hardwareConfiguration.split('\u001d') }), supplies: (statuses) => (statuses && statuses.substring && { suppliesStatus: statuses.substring(2).split('\u001d').reduce((prev, cur) => { var device = cur && cur.substring && map.devices[cur.substring(0, 1)]; device && (prev[device] = cur.substring(1).split('').map((status) => map.suppliesStatus[status])); return prev; }, {}) }), fitness: (statuses) => (statuses && statuses.substring && { fitnessStatus: statuses.substring(2).split('\u001d').reduce((prev, cur) => { var device = cur && cur.substring && map.devices[cur.substring(0, 1)]; device && (prev[device] = cur.substring(1).split('').map((status) => map.severities[status])); return prev; }, {}) }), sensor: (sensor, tamper) => (sensor && sensor.substring && tamper && tamper.substring && parsers.sensors(' ' + sensor.substring(2) + tamper.substring(1))), release: (release, software) => ({ release: release && release.substring && release.substring(2).match(/.{1,2}/g), software: software && software.substring && software.substring(1) }), optionDigits: (optionDigits) => ({ optionDigits: optionDigits && optionDigits.substring && optionDigits.substring(2).split('') }), depositDefition: (acceptedCashItems) => ({ acceptedCashItems: acceptedCashItems && acceptedCashItems.substring && acceptedCashItems.substring(2).match(/.{11}/g) }), // devices clock: (status) => (status && status.substring && { deviceStatusDescription: map.clockStatuses[status.substring(0, 1)] }), power: (config) => ({ config }), cardReader: (status) => (status && status.substring && { deviceStatusDescription: map.cardReaderStatuses[status.substring(0, 1)] }), cashHandler: (status) => (status && status.substring && { deviceStatusDescription: map.cashHandlerStatuses[status.substring(0, 1)], dispensed1: status.substring(1, 3), dispensed2: status.substring(3, 5), dispensed3: status.substring(5, 7), dispensed4: status.substring(7, 9) }), depository: (status) => (status && status.substring && { deviceStatusDescription: map.depositoryStatuses[status.substring(0, 1)] }), receiptPrinter: (status) => (status && status.substring && { deviceStatusDescription: map.receiptPrinterStatuses[status.substring(0, 1)] }), journalPrinter: (status) => (status && status.substring && { deviceStatusDescription: map.journalPrinterStatuses[status.substring(0, 1)] }), encryptor: (status) => (status && status.substring && { deviceStatusDescription: map.encryptorStatuses[status.substring(0, 1)] }), camera: (status) => ({}), sensors: (status, skipSession) => (status && status.substring && { deviceStatusDescription: map.sensorStatuses[status.substring(0, 1)], supervisorMode: map.sensors[status.substring(1, 2)], vibration: (status.substring(0, 1) !== '2') && map.sensors[status.substring(2, 3)], door: map.sensors[status.substring(3, 4)], silentSignal: map.sensors[status.substring(4, 5)], electronicsEnclosure: map.sensors[status.substring(5, 6)], depositBin: map.sensors[status.substring(6, 7)], cardBin: map.sensors[status.substring(7, 8)], rejectBin: map.sensors[status.substring(8, 9)], cassette1: map.sensors[status.substring(9, 10)], cassette2: map.sensors[status.substring(10, 11)], cassette3: map.sensors[status.substring(11, 12)], cassette4: map.sensors[status.substring(12, 13)], session: skipSession ? undefined : { cassettes: [ {sensor: map.sensors[status.substring(9, 10)]}, {sensor: map.sensors[status.substring(10, 11)]}, {sensor: map.sensors[status.substring(11, 12)]}, {sensor: map.sensors[status.substring(12, 13)]} ] }, coinDispenser: map.sensors[status.substring(13, 14)], coinHopper1: map.sensors[status.substring(14, 15)], coinHopper2: map.sensors[status.substring(15, 16)], coinHopper3: map.sensors[status.substring(16, 17)], coinHopper4: map.sensors[status.substring(17, 18)], cpmPockets: map.sensors[status.substring(18, 19)] }), supervisorKeys: (status) => ({ menu: status }), statementPrinter: (status) => ({ deviceStatusDescription: map.statementPrinterStatuses[status.substring(0, 1)] }), coinDispenser: (status) => ({ deviceStatusDescription: map.coinDispenserStatuses[status.substring(0, 1)], coinsDispensed: status.substring(1).match(/.{1,2}/g) }), voiceGuidance: (status) => ({}), noteAcceptor: (status) => (status && status.substring && { deviceStatusDescription: map.noteAcceptorStatuses[status.substring(0, 1)] }), // message classes unsolicitedStatus: function(type, luno, reserved, deviceIdentifierAndStatus, errorSeverity, diagnosticStatus, suppliesStatus) { return this.fault(deviceIdentifierAndStatus, errorSeverity, diagnosticStatus, suppliesStatus); }, solicitedStatus: function(type, luno, reserved, descriptor, status) { var fn = descriptor && map.descriptors[descriptor] && this[map.descriptors[descriptor]]; if (typeof fn === 'function') { return merge({ luno, descriptor: map.descriptors[descriptor] }, fn.apply(this, Array.prototype.slice.call(arguments, 4))); } }, encryptorIniData: (type, luno, reserved, identifier, info) => { switch (identifier) { case '4': return { masterKvv: info.substring && info.substring(0, 6) }; case '3': return { newKvv: info.substring && info.substring(0, 6) }; } return {}; }, uploadEjData: (type, luno, reserved1, reserved2, journalData) => ({ type, luno, journalData }), lastTransaction: fields => { var field2 = fields.find(field => field.substring(0, 1) === '2'); field2 = field2 && field2.match(/^2(\d{4})(\d)(\d{5})(\d{5})(\d{5})(\d{5})/); return field2 && { sernum: field2[1], status: field2[2], notes1: parseInt(field2[3]), notes2: parseInt(field2[4]), notes3: parseInt(field2[5]), notes4: parseInt(field2[6]) }; }, smartCardData: (fields) => { // There are 16 available CAM flags.These are encoded as the bits in two bytes, and are converted to ASCII hex(four bytes) for transmission. Each can have the value 0x0 or 0x1 var smartCardData = fields.find(field => field.substring(0, 4) === '5CAM'); smartCardData = (smartCardData && smartCardData.substring(4)) || ''; var camFlags = new Buffer(smartCardData.substring(0, 4), 'hex'); var emvTags = smartCardData.substring(4); return Object.assign( {}, (!camFlags.length ? {} : parsers.camFlagsDecode(camFlags)), (!emvTags.length ? {} : {emvTags: emv.tagsDecode(emvTags, {})}) ); }, camFlagsDecode: (buffer) => { var b1 = buffer.slice(0, 1); var a = []; if (b1.length) { b1 = b1.readInt8(); a.push(0); a.push(((b1 & 2) === 2) ? 1 : 0); a.push(((b1 & 4) === 4) ? 1 : 0); a.push(((b1 & 8) === 8) ? 1 : 0); a.push(((b1 & 16) === 16) ? 1 : 0); a.push(((b1 & 32) === 32) ? 1 : 0); a.push(0); a.push(0); } var b2 = buffer.slice(1, 2); var b = []; if (b2.length) { b2 = b2.readInt8(); b.push(0); b.push(0); b.push(0); b.push(((b2 & 8) === 8) ? 1 : 0); b.push(0); b.push(((b2 & 32) === 32) ? 1 : 0); b.push(((b2 & 64) === 64) ? 1 : 0); b.push(((b2 & 128) === 128) ? 1 : 0); } return {camFlags: [a, b]}; }, pinBlock: pin => pin && pin.split && pin.split('').map((c) => ({ ':': 'A', ';': 'B', '<': 'C', '=': 'D', '>': 'E', '?': 'F' }[c] || c)).join(''), pinBlockNew: fields => { var result = fields.find(field => field.substring(0, 1) === 'U'); return result && parsers.pinBlock(result.substr(1, 16)); }, transaction: function(type, luno, reserved, timeVariantNumber, trtfmcn, track2, track3, opcode, amount, pinBlock, bufferB, bufferC) { var args1 = Array.prototype.slice.call(arguments, 12); return Object.assign({ type, luno, reserved, timeVariantNumber, topOfReceipt: trtfmcn && trtfmcn.substring && trtfmcn.substring(0, 1), coordination: trtfmcn && trtfmcn.substring && trtfmcn.substring(1, 2), track2, track3, opcode: opcode && opcode.split && opcode.split(''), amount, pinBlock: parsers.pinBlock(pinBlock), pinBlockNew: parsers.pinBlockNew(args1), bufferB, bufferC, lastTransactionData: parsers.lastTransaction(args1) }, parsers.smartCardData(args1)); }, transactionReply: (type, luno, timeVariantNumber, nextState, notes, sernumFunction, coordinationCardPrinter) => ({ type, luno, timeVariantNumber, nextState, notes, sernum: sernumFunction && sernumFunction.substring && sernumFunction.substring(0, 4), function: sernumFunction && sernumFunction.substring && sernumFunction.substring(4, 5), screen: sernumFunction && sernumFunction.substring && sernumFunction.substring(5, 8), screenUpdate: sernumFunction && sernumFunction.substring && sernumFunction.substring(8), coordination: coordinationCardPrinter && coordinationCardPrinter.substring && coordinationCardPrinter.substring(0, 1), cardReturn: coordinationCardPrinter && coordinationCardPrinter.substring && coordinationCardPrinter.substring(1, 2), printer: coordinationCardPrinter && coordinationCardPrinter.substring && coordinationCardPrinter.substring(2, 3), printerData: coordinationCardPrinter && coordinationCardPrinter.substring && coordinationCardPrinter.substring(3) }), // sim keyReadKvv: () => ({}), // sim keyChangeTak: () => ({}), // sim keyChangeTpk: () => ({}), // sim currencyMappingLoad: () => ({}), // sim sendConfigurationId: () => ({}), // sim paramsLoadEnhanced: () => ({}), // sim dateTimeLoad: () => ({}), // sim screenDataLoad: () => ({}), // sim stateTableLoad: () => ({}), // sim fitDataLoad: () => ({}), // sim configIdLoad: () => ({}), // sim sendConfiguration: () => ({}), // sim sendConfigurationHardware: () => ({}), // sim sendConfigurationSuplies: () => ({}), // sim sendConfigurationFitness: () => ({}), // sim sendConfigurationSensor: () => ({}), // sim sendConfigurationRelease: () => ({}), // sim sendConfigurationOptionDigits: () => ({}), // sim sendConfigurationDepositDefinition: () => ({}), // sim emvCurrency: () => ({}), // sim emvTransaction: () => ({}), // sim emvLanguage: () => ({}), // sim emvTerminal: () => ({}), // sim emvApplication: () => ({}), // sim sendSupplyCounters: () => ({}), // sim goInService: () => ({}), // sim goOutOfServiceTemp: () => ({}), // sim goOutOfService: () => ({}), // sim ejOptions: () => ({}), // sim ejAck: () => ({}) // sim }; NDC.prototype.decode = function(buffer, $meta, context) { var message = {}; var bufferString = buffer.toString(); if (buffer.length > 0) { var tokens = bufferString.split(this.fieldSeparator); var command; switch (tokens[0]) { case '1': case '3': command = this.codes[`${tokens[0]}|${tokens[3]}`]; break; case '8': command = this.codes[`${tokens[0]}${tokens[2]}|`]; break; case '6': command = this.codes[`${tokens[0]}|${tokens[3].charAt(0)}`]; break; default: command = this.codes[`${tokens[0]}|`]; break; } if (command) { $meta.mtid = command.mtid; $meta.method = (command.mtid === 'response' ? '' : 'aptra.') + command.method; message = {session: context.session}; switch ($meta.method) { case 'solicitedStatus': if (tokens[3] != null && (tokens[3].length === 8 || tokens[3].length === 0)) { // mac is active message.timeVariantNumber = tokens[3]; tokens.splice(3, 1); }; if (tokens[3] === 'B' || tokens[3] === '8') { context.traceTransactionReady = context.traceTransactionReady || 1; $meta.trace = 'trn:' + context.traceTransactionReady; context.traceTransactionReady += 1; context.traceTerminal = 1; context.traceCentral = 1; if (tokens[3] === '8') { message.transactionTimeout = (context.session && context.session.transactionTimeout) || 55; context.transactionReplyTime = hrtime()[0] + message.transactionTimeout; message.transactionRequestId = context.transactionRequestId = (context.transactionRequestId || 0) + 1; } } else { context.traceTerminal = context.traceTerminal || 1; $meta.trace = 'req:' + context.traceTerminal; context.traceTerminal += 1; } break; case 'encryptorIniData': context.traceTerminalKeys = context.traceTerminalKeys || 1; $meta.trace = 'keys:' + context.traceTerminalKeys; context.traceTerminalKeys += 1; break; case 'aptra.transactionReply': // sim $meta.mtid = 'response'; context.traceTransactionReady = context.traceTransactionReady || 1; $meta.trace = 'trn:' + context.traceTransactionReady; context.traceTransactionReady += 1; break; case 'aptra.transaction': message.transactionTimeout = (context.session && context.session.transactionTimeout) || 55; context.transactionReplyTime = hrtime()[0] + message.transactionTimeout; message.transactionRequestId = context.transactionRequestId = (context.transactionRequestId || 0) + 1; break; } var fn = parsers[command.method]; if (typeof fn === 'function') { merge(message, fn.apply(parsers, tokens)); } else { var e = new Error('No parser found for message: ' + command.method); e.type = 'aptra.decode'; throw e; } message.tokens = tokens; } else { $meta.mtid = 'error'; message.type = 'aptra.unknownMessageClass'; message.message = 'Received unknown message class: ' + tokens[0]; } } return message; }; NDC.prototype.encode = function(message, $meta, context) { if (typeof this.val === 'function') { this.val(message); } var bufferString = ''; switch ($meta.opcode) { case 'terminalCommand': case 'goInService': case 'goOutOfService': case 'goOutOfServiceTemp': case 'sendConfigurationId': case 'sendSupplyCounters': case 'paramsLoadEnhanced': case 'currencyMappingLoad': case 'stateTableLoad': case 'screenDataLoad': case 'fitDataLoad': case 'dateTimeLoad': case 'configIdLoad': case 'sendConfiguration': case 'sendConfigurationHardware': case 'sendConfigurationSuplies': case 'sendConfigurationFitness': case 'sendConfigurationSensor': case 'sendConfigurationRelease': case 'sendConfigurationEnhanced': case 'sendConfigurationOptionDigits': case 'sendConfigurationDepositDefinition': case 'emvCurrency': case 'emvTransaction': case 'emvLanguage': case 'emvTerminal': case 'emvApplication': context.traceCentral = context.traceCentral || 1; $meta.trace = 'req:' + context.traceCentral; context.traceCentral += 1; break; case 'keyReadKvv': case 'keyChangeTak': case 'keyChangeTpk': context.traceCentralKeys = context.traceCentralKeys || 1; $meta.trace = 'keys:' + context.traceCentralKeys; context.traceCentralKeys += 1; break; case 'transaction': // sim context.traceTransaction = context.traceTransaction || 1; $meta.trace = 'trn:' + context.traceTransaction; context.traceTransaction += 1; break; case 'transactionReply': if (message.transactionRequestId === context.transactionRequestId && context.transactionReplyTime > hrtime()[0]) { context.traceTransaction = context.traceTransaction || 1; $meta.trace = 'trn:' + context.traceTransaction; context.traceTransaction += 1; } else { var e = new Error('Transaction timed out'); e.type = 'aptra.timeout'; throw e; } break; } message.session && merge(context, {session: message.session}); var command = this.messageFormat[$meta.opcode]; if (command) { merge(message, command.values); // bufferString += command.messageClass; command.fieldsSplit.forEach((field) => { if (field.length) { switch (field) { case 'FS': bufferString += this.fieldSeparator; break; case 'GS': bufferString += this.groupSeparator; break; case 'printers': bufferString += (message[field] || []).map(printer => printer.printer + printer.printerData).join(this.groupSeparator); break; default: bufferString += message[field] || ''; } } }); if (message.emvTags || message.camFlags) { bufferString += this.fieldSeparator + packSmartCardData(message.camFlags, message.emvTags); } if (message.mac) { bufferString += this.fieldSeparator + message.mac; } return new Buffer(bufferString); } }; module.exports = NDC; // todo checkMac // todo CashHandlerAlert / CassetteSupplyStatus // todo ReceiptPrinterAlert / JournalPrinterAlert / PaperSupplyStatus / PrinterPartStatus // todo OtherDeviceFaultAlert // todo handle when solicited status is not matched to request