diff --git a/lib/bill-validator.js b/lib/bill-validator.js new file mode 100644 index 000000000..b664b1db7 --- /dev/null +++ b/lib/bill-validator.js @@ -0,0 +1,26 @@ +const pickClass = ({ deviceType }, mock) => { + if (mock) { + switch (deviceType) { + case 'hcm2': return require('./mocks/hcm2/hcm2') + case 'gsr50': return require('./mocks/gsr50/gsr50') + default: return require('./mocks/id003') + } + } + + switch (deviceType) { + case 'genmega': return require('./genmega/genmega-validator/genmega-validator') + case 'cashflowSc': return require('./mei/cashflow_sc') + case 'bnrAdvance': return require('./mei/bnr_advance') + case 'ccnet': return require('./ccnet/ccnet') + case 'hcm2': return require('./hcm2/hcm2') + case 'gsr50': return require('./gsr50/gsr50') + default: return require('./id003/id003') + } +} + +const load = (deviceConfig, mock = false) => { + const validatorClass = pickClass(deviceConfig, mock) + return validatorClass.factory(deviceConfig) +} + +module.exports = { load } diff --git a/lib/brain.js b/lib/brain.js index 82aa9c2ec..782a754fa 100644 --- a/lib/brain.js +++ b/lib/brain.js @@ -43,6 +43,7 @@ const { getLowestAmountPerRequirement, getAmountToHardLimit, getTriggered } = re const { ORDERED_REQUIREMENTS, REQUIREMENTS } = require('./compliance/triggers/consts') const printerLoader = require('./printer/loader') +const billValidator = require('./bill-validator') const BigNumber = BN.klass let transitionTime @@ -274,30 +275,8 @@ Brain.prototype.prunePending = function prunePending (txs) { .then(() => pendingTxs.length) } -Brain.prototype.selectBillValidatorClass = function selectBillValidatorClass () { - const billValidator = this.rootConfig.billValidator.deviceType - if (commandLine.mockBillValidator) { - switch (billValidator) { - case 'hcm2': return require('./mocks/hcm2/hcm2') - case 'gsr50': return require('./mocks/gsr50/gsr50') - default: return require('./mocks/id003') - } - } - - switch (billValidator) { - case 'genmega': return require('./genmega/genmega-validator/genmega-validator') - case 'cashflowSc': return require('./mei/cashflow_sc') - case 'bnrAdvance': return require('./mei/bnr_advance') - case 'ccnet': return require('./ccnet/ccnet') - case 'hcm2': return require('./hcm2/hcm2') - case 'gsr50': return require('./gsr50/gsr50') - default: return require('./id003/id003') - } -} - Brain.prototype.loadBillValidator = function loadBillValidator () { - const billValidatorClass = this.selectBillValidatorClass() - return billValidatorClass.factory(this.rootConfig.billValidator) + return billValidator.load(this.rootConfig.billValidator, !!commandLine.mockBillValidator) } Brain.prototype.billValidatorHasShutter = function billValidatorHasShutter () { diff --git a/lib/hardware-testing/bill-validator.js b/lib/hardware-testing/bill-validator.js new file mode 100644 index 000000000..0b844f01d --- /dev/null +++ b/lib/hardware-testing/bill-validator.js @@ -0,0 +1,120 @@ +const { load } = require("../bill-validator") + +const DEVICE_CONFIG = { + deviceType: "cashflowSc", + rs232: { + device: "/dev/ttyUSB0", + }, +} + +const FIAT_CODE = 'EUR' + +const BOXES = { + numberOfCashboxes: 1, + cassettes: 0, + recyclers: 0, +} + +const addEventListeners = (validator, reject, eventListenersToAdd) => { + const allEvents = [ + 'actionRequiredMaintenance', + 'billsAccepted', + 'billsRead', + 'billsRejected', + 'billsValid', + 'cashSlotRemoveBills', + 'disconnected', + 'enabled', + 'error', + 'jam', + 'leftoverBillsInCashSlot', + 'stackerClosed', + 'stackerOpen', + 'standby', + ] + + const handleUnexpected = eventName => (...eventArguments) => { + const error = new Error("Unexpected event received") + error.eventName = eventName + error.eventArguments = [...eventArguments] + reject(error) + } + + allEvents.forEach( + ev => validator.on(ev, eventListenersToAdd[ev] ?? handleUnexpected) + ) +} + +const doFinally = validator => () => { + validator.removeAllListeners() + validator.disable() +} + +const testEnable = validator => () => new Promise( + (resolve, reject) => { + let accepted = false + const enable = () => { + accepted = false + validator.enable() + } + + addEventListeners(validator, reject, { + billsAccepted: () => { accepted = true }, + billsRead: bills => resolve({ accepted, bills }), + billsRejected: () => enable(), + }) + + enable() + } +).finally(doFinally(validator)) + +const steps = () => + new Promise((resolve, reject) => { + const validator = load(DEVICE_CONFIG, false) + validator.setFiatCode(FIAT_CODE) + validator.run( + err => err ? reject(err) : resolve(validator), + BOXES, + ) + }) + .then(validator => [ + { + name: 'enableReject', + instructionMessage: `Try to insert a ${FIAT_CODE} bill...`, + confirmationMessage: "Did the validator retrieve the bill?", + test: testEnable(validator), + }, + + { + name: 'reject', + confirmationMessage: "Did the validator reject the bill?", + test: () => + new Promise((resolve, reject) => { + addEventListeners(validator, reject, { + billsRejected: () => resolve(), + }) + validator.reject() + }).finally(doFinally(validator)), + }, + + { + name: 'enableStack', + instructionMessage: `Try to insert a ${FIAT_CODE} bill...`, + confirmationMessage: "Did the validator retrieve the bill?", + test: testEnable(validator), + }, + + { + name: 'stack', + confirmationMessage: "Did the validator stack the bill?", + test: () => + new Promise((resolve, reject) => { + addEventListeners(validator, reject, { + billsValid: () => resolve(), + }) + validator.stack() + }).finally(doFinally(validator)), + }, + ]) + +module.exports = steps diff --git a/lib/hardware-testing/index.js b/lib/hardware-testing/index.js new file mode 100644 index 000000000..6c4569030 --- /dev/null +++ b/lib/hardware-testing/index.js @@ -0,0 +1,71 @@ +const readline = require('./readline') + +const PERIPHERALS = { + printer: require('./printer'), + billValidator: require('./bill-validator'), +} + + +const defaultPre = () => null +const defaultPost = (error, result) => error ? { error } : { result } + +const reducer = (acc, { + name, + instructionMessage, + confirmationMessage, + skipConfirm = false, + keepGoing = false, + pre = defaultPre, + test, + post = defaultPost, +}) => + acc + .then(([cont, report]) => { + if (!cont) return [cont, report] + return (instructionMessage ? + readline.instruct(instructionMessage) : + Promise.resolve() + ) + .then(() => { + const args = pre() + return test(args) + }) + .then( + result => [true, post(null, result)], + error => [!!keepGoing, post(error, null)], + ) + .then(([cont, result]) => + (skipConfirm ? Promise.resolve(true) : readline.confirm(confirmationMessage)) + .then(worked => [cont, result, worked]) + .catch(error => [false, result, false]) + ) + .then(([cont, result, confirmed]) => { + report = Object.assign(report, { + [name]: { result, confirmed } + }) + return [cont && (keepGoing || confirmed), report] + }) + }) + + +const runSteps = steps => + steps.reduce(reducer, Promise.resolve([true, {}])) + +const runPeripheral = ([peripheral, steps]) => + steps() + .then( + steps => steps === null ? { skipped: true } : runSteps(steps), + error => { error } + ) + .then(result => [peripheral, result]) + +const runPeripherals = peripherals => { + readline.create() + return Promise.all(Object.entries(peripherals).map(runPeripheral)) + .then(Object.fromEntries) + .finally(() => readline.close()) +} + +runPeripherals(PERIPHERALS) + .then(results => console.log(JSON.stringify(results, null, 2))) + .catch(console.log) diff --git a/lib/hardware-testing/printer.js b/lib/hardware-testing/printer.js new file mode 100644 index 000000000..e90086d06 --- /dev/null +++ b/lib/hardware-testing/printer.js @@ -0,0 +1,83 @@ +const { load } = require('../printer/loader') + +const PRINTER_CONFIG = { + model: "genmega", + address: "/dev/ttyS4", +} + +const RECEIPT_DATA = { + operatorInfo: { + name: "operator name", + website: "operator website", + email: "operator email", + phone: "operator phone", + companyNumber: "operator companyNumber", + }, + location: "location", + customer: "customer", + session: "session", + time: "14:29", + direction: 'Cash-in', + fiat: "200 EUR", + crypto: "1.234 BTC", + rate: "1 BTC = 123.456 EUR", + address: "bc1qdg6hsh9w8xwdz66khec3fl8c9wva0ys2tc277f", + txId: "07b2c12b88a2208f21fefc0a65dd045e073c566e47b721c51238886702539283", +} + +const RECEIPT_CONFIG = Object.fromEntries([ + "operatorWebsite", + "operatorEmail", + "operatorPhone", + "companyNumber", + "machineLocation", + "customerNameOrPhoneNumber", + "exchangeRate", + "addressQRCode", +].map(k => [k, true])) + +const WALLET = { privateKey: "private key" } + +const steps = () => load() + .then( + printer => [ + { + name: 'checkStatus', + skipConfirm: true, + pre: () => PRINTER_CONFIG, + test: printerCfg => printer.checkStatus(printerCfg), + }, + + { + name: 'printReceipt', + confirmationMessage: "Was a receipt printed?", + keepGoing: true, + pre: () => ({ + receiptData: RECEIPT_DATA, + printerCfg: PRINTER_CONFIG, + receiptCfg: RECEIPT_CONFIG, + }), + test: ({ receiptData, printerCfg, receiptCfg }) => + printer.printReceipt(receiptData, printerCfg, receiptCfg), + }, + + { + name: 'printWallet', + confirmationMessage: "Was a wallet printed?", + keepGoing: true, + pre: () => ({ + wallet: WALLET, + printerCfg: PRINTER_CONFIG, + cryptoCode: 'BTC', + }), + test: ({ wallet, printerCfg, cryptoCode }) => + printer.printWallet(wallet, printerCfg, cryptoCode), + }, + ], + error => + error.message === 'noPrinterConfiguredError' ? + null : + Promise.reject(error) + ) + +module.exports = steps diff --git a/lib/hardware-testing/readline.js b/lib/hardware-testing/readline.js new file mode 100644 index 000000000..b3bdd2749 --- /dev/null +++ b/lib/hardware-testing/readline.js @@ -0,0 +1,61 @@ +const readline = require('node:readline'); +const { stdin: input, stdout: output } = require('node:process'); + +let rl = null + + +const create = () => { + rl = readline.createInterface({ + input, + output, + terminal: true, + }) +} + + +const close = () => { + rl.close() + rl = null +} + + +const instruct = msg => new Promise((resolve, reject) => { + try { + rl.write(`\n${msg}\n`) + return resolve() + } catch (error) { + return reject(error) + } +}) + + +const confirmOnce = msg => new Promise((resolve, reject) => + rl.question( + `${msg} ([y]es/[n]) `, + answer => + ["y", "yes"].includes(answer) ? resolve(true) : + ["n", "no"].includes(answer) ? resolve(false) : + reject(true) + ) +) + +const confirm = async msg => { + msg ??= "Did it work?" + const retry = true + while (retry) { + try { + return await confirmOnce(msg) + } catch (newRetry) { + retry = typeof(newRetry) === 'boolean' && newRetry + } + } + return false +} + + +module.exports = { + create, + instruct, + confirm, + close, +} diff --git a/lib/mei/cashflow_sc.js b/lib/mei/cashflow_sc.js index 9e07f9572..9e4fd58ff 100644 --- a/lib/mei/cashflow_sc.js +++ b/lib/mei/cashflow_sc.js @@ -218,7 +218,7 @@ function parseStatus (data) { (data[0].returned || data[1].cheated || data[1].rejected) ? 'billsRejected' : data[1].jammed ? 'jam' : !data[1].cassetteAttached ? 'stackerOpen' : - data[0].idling ? 'idle' : + data[0].idling ? 'standby' : null }