Reserve notes for redeem
This commit is contained in:
parent
1488a60a60
commit
67e12b19cb
6 changed files with 235 additions and 93 deletions
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible Node.js debug attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Program",
|
||||||
|
"program": "${workspaceRoot}/bin/lamassu-server",
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"args": ["--mockSms"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
95
lib/cash-out-helper.js
Normal file
95
lib/cash-out-helper.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const db = require('./db')
|
||||||
|
const T = require('./time')
|
||||||
|
const BN = require('./bn')
|
||||||
|
|
||||||
|
const REDEEMABLE_AGE = T.day
|
||||||
|
|
||||||
|
module.exports = {redeemableTxs, toObj, toDb}
|
||||||
|
|
||||||
|
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||||
|
|
||||||
|
function convertBigNumFields (obj) {
|
||||||
|
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
|
||||||
|
? value.toString()
|
||||||
|
: value
|
||||||
|
|
||||||
|
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
|
||||||
|
? key + '#'
|
||||||
|
: key
|
||||||
|
|
||||||
|
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertField (key) {
|
||||||
|
return _.snakeCase(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDbBills (tx) {
|
||||||
|
const bills = tx.bills
|
||||||
|
if (_.isEmpty(bills)) return tx
|
||||||
|
|
||||||
|
return _.assign(tx, {
|
||||||
|
provisioned1: bills[0].provisioned,
|
||||||
|
provisioned2: bills[1].provisioned,
|
||||||
|
denomination1: bills[0].denomination,
|
||||||
|
denomination2: bills[1].denomination
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDb (tx) {
|
||||||
|
const massager = _.flow(convertBigNumFields, addDbBills,
|
||||||
|
_.omit(['direction', 'bills']), _.mapKeys(convertField))
|
||||||
|
|
||||||
|
return massager(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toObj (row) {
|
||||||
|
if (!row) return null
|
||||||
|
|
||||||
|
const keys = _.keys(row)
|
||||||
|
let newObj = {}
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
const objKey = _.camelCase(key)
|
||||||
|
if (key === 'crypto_atoms' || key === 'fiat') {
|
||||||
|
newObj[objKey] = BN(row[key])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newObj[objKey] = row[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
newObj.direction = 'cashOut'
|
||||||
|
|
||||||
|
const billFields = ['denomination1', 'denomination2', 'provisioned1', 'provisioned2']
|
||||||
|
|
||||||
|
if (_.every(_.isNil, _.at(billFields, newObj))) return newObj
|
||||||
|
if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values')
|
||||||
|
|
||||||
|
const bills = [
|
||||||
|
{
|
||||||
|
denomination: newObj.denomination1,
|
||||||
|
provisioned: newObj.provisioned1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
denomination: newObj.denomination2,
|
||||||
|
provisioned: newObj.provisioned2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return _.set('bills', bills, _.omit(billFields, newObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
function redeemableTxs (deviceId) {
|
||||||
|
const sql = `select * from cash_out_txs
|
||||||
|
where device_id=$1
|
||||||
|
and redeem=$2
|
||||||
|
and dispense=$3
|
||||||
|
and provisioned_1 is not null
|
||||||
|
and (extract(epoch from (now() - greatest(created, confirmation_time))) * 1000) < $4`
|
||||||
|
|
||||||
|
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
|
||||||
|
.then(_.map(toObj))
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,11 @@ const _ = require('lodash/fp')
|
||||||
const pgp = require('pg-promise')()
|
const pgp = require('pg-promise')()
|
||||||
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const BN = require('./bn')
|
|
||||||
const billMath = require('./bill-math')
|
const billMath = require('./bill-math')
|
||||||
const T = require('./time')
|
const T = require('./time')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const plugins = require('./plugins')
|
const plugins = require('./plugins')
|
||||||
|
const helper = require('./cash-out-helper')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
post,
|
post,
|
||||||
|
|
@ -16,10 +16,8 @@ module.exports = {
|
||||||
cancel
|
cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
const UPDATEABLE_FIELDS = ['txHash', 'status', 'dispense', 'dispenseConfirmed',
|
||||||
|
'notified', 'redeem', 'phone', 'error', 'swept']
|
||||||
const UPDATEABLE_FIELDS = ['txHash', 'status', 'dispense', 'notified', 'redeem',
|
|
||||||
'phone', 'error', 'swept']
|
|
||||||
|
|
||||||
const STALE_INCOMING_TX_AGE = T.week
|
const STALE_INCOMING_TX_AGE = T.week
|
||||||
const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes
|
const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes
|
||||||
|
|
@ -27,6 +25,9 @@ const MAX_NOTIFY_AGE = 2 * T.days
|
||||||
const MIN_NOTIFY_AGE = 5 * T.minutes
|
const MIN_NOTIFY_AGE = 5 * T.minutes
|
||||||
const INSUFFICIENT_FUNDS_CODE = 570
|
const INSUFFICIENT_FUNDS_CODE = 570
|
||||||
|
|
||||||
|
const toObj = helper.toObj
|
||||||
|
const toDb = helper.toDb
|
||||||
|
|
||||||
function httpError (msg, code) {
|
function httpError (msg, code) {
|
||||||
const err = new Error(msg)
|
const err = new Error(msg)
|
||||||
err.name = 'HTTPError'
|
err.name = 'HTTPError'
|
||||||
|
|
@ -128,27 +129,6 @@ function diff (oldTx, newTx) {
|
||||||
return updatedTx
|
return updatedTx
|
||||||
}
|
}
|
||||||
|
|
||||||
function toObj (row) {
|
|
||||||
if (!row) return null
|
|
||||||
|
|
||||||
const keys = _.keys(row)
|
|
||||||
let newObj = {}
|
|
||||||
|
|
||||||
keys.forEach(key => {
|
|
||||||
const objKey = _.camelCase(key)
|
|
||||||
if (key === 'crypto_atoms' || key === 'fiat') {
|
|
||||||
newObj[objKey] = BN(row[key])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newObj[objKey] = row[key]
|
|
||||||
})
|
|
||||||
|
|
||||||
newObj.direction = 'cashOut'
|
|
||||||
|
|
||||||
return newObj
|
|
||||||
}
|
|
||||||
|
|
||||||
function upsert (oldTx, tx) {
|
function upsert (oldTx, tx) {
|
||||||
if (!oldTx) {
|
if (!oldTx) {
|
||||||
return insert(tx)
|
return insert(tx)
|
||||||
|
|
@ -159,27 +139,6 @@ function upsert (oldTx, tx) {
|
||||||
.then(newTx => [oldTx, newTx])
|
.then(newTx => [oldTx, newTx])
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertBigNumFields (obj) {
|
|
||||||
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
|
|
||||||
? value.toString()
|
|
||||||
: value
|
|
||||||
|
|
||||||
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
|
|
||||||
? key + '#'
|
|
||||||
: key
|
|
||||||
|
|
||||||
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertField (key) {
|
|
||||||
return _.snakeCase(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDb (tx) {
|
|
||||||
const massager = _.flow(convertBigNumFields, _.omit(['direction', 'bills']), _.mapKeys(convertField))
|
|
||||||
return massager(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
function insert (tx) {
|
function insert (tx) {
|
||||||
const dbTx = toDb(tx)
|
const dbTx = toDb(tx)
|
||||||
|
|
||||||
|
|
@ -191,7 +150,7 @@ function insert (tx) {
|
||||||
function update (tx, changes) {
|
function update (tx, changes) {
|
||||||
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
||||||
|
|
||||||
const dbChanges = toDb(tx)
|
const dbChanges = toDb(changes)
|
||||||
const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') +
|
const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') +
|
||||||
pgp.as.format(' where id=$1', [tx.id])
|
pgp.as.format(' where id=$1', [tx.id])
|
||||||
|
|
||||||
|
|
@ -223,6 +182,16 @@ function updateCassettes (tx) {
|
||||||
return db.none(sql, values)
|
return db.none(sql, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
|
||||||
|
const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published']) &&
|
||||||
|
_.includes(newTx.status, ['authorized', 'instant', 'confirmed'])
|
||||||
|
|
||||||
|
const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized']) &&
|
||||||
|
_.includes(newTx.status, ['instant', 'confirmed'])
|
||||||
|
|
||||||
|
return isZeroConf ? isAuthorized() : isConfirmed()
|
||||||
|
}
|
||||||
|
|
||||||
function preProcess (oldTx, newTx, pi) {
|
function preProcess (oldTx, newTx, pi) {
|
||||||
if (!oldTx) {
|
if (!oldTx) {
|
||||||
return pi.isHd(newTx)
|
return pi.isHd(newTx)
|
||||||
|
|
@ -246,10 +215,14 @@ function preProcess (oldTx, newTx, pi) {
|
||||||
if (!oldTx) return updatedTx
|
if (!oldTx) return updatedTx
|
||||||
|
|
||||||
if (updatedTx.status !== oldTx.status) {
|
if (updatedTx.status !== oldTx.status) {
|
||||||
|
const isZeroConf = pi.isZeroConf(updatedTx)
|
||||||
|
if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) pi.sell(updatedTx)
|
||||||
|
|
||||||
const rec = {
|
const rec = {
|
||||||
to_address: updatedTx.toAddress,
|
to_address: updatedTx.toAddress,
|
||||||
tx_hash: updatedTx.txHash
|
tx_hash: updatedTx.txHash
|
||||||
}
|
}
|
||||||
|
|
||||||
return logAction(updatedTx.status, rec, updatedTx)
|
return logAction(updatedTx.status, rec, updatedTx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,23 +246,29 @@ function preProcess (oldTx, newTx, pi) {
|
||||||
function postProcess (txVector, pi) {
|
function postProcess (txVector, pi) {
|
||||||
const [oldTx, newTx] = txVector
|
const [oldTx, newTx] = txVector
|
||||||
|
|
||||||
if (newTx.dispense && !oldTx.dispense) {
|
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
|
||||||
return pi.buildCassettes()
|
return pi.buildAvailableCassettes(newTx.id)
|
||||||
.then(cassettes => {
|
.then(cassettes => {
|
||||||
pi.sell(newTx)
|
|
||||||
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
|
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
|
||||||
|
console.log('DEBUG130: %j', cassettes.cassettes)
|
||||||
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
||||||
return _.set('bills', bills, newTx)
|
return bills
|
||||||
})
|
})
|
||||||
.then(tx => {
|
.then(bills => {
|
||||||
|
const provisioned1 = bills[0].provisioned
|
||||||
|
const provisioned2 = bills[1].provisioned
|
||||||
|
const denomination1 = bills[0].denomination
|
||||||
|
const denomination2 = bills[1].denomination
|
||||||
|
|
||||||
const rec = {
|
const rec = {
|
||||||
provisioned_1: tx.bills[0].provisioned,
|
provisioned_1: provisioned1,
|
||||||
provisioned_2: tx.bills[1].provisioned,
|
provisioned_2: provisioned2,
|
||||||
denomination_1: tx.bills[0].denomination,
|
denomination_1: denomination1,
|
||||||
denomination_2: tx.bills[1].denomination
|
denomination_2: denomination2
|
||||||
}
|
}
|
||||||
|
|
||||||
return logAction('provisionNotes', rec, tx)
|
return logAction('provisionNotes', rec, newTx)
|
||||||
|
.then(_.constant({bills}))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return logError('provisionNotesError', err, newTx)
|
return logError('provisionNotesError', err, newTx)
|
||||||
|
|
@ -297,7 +276,7 @@ function postProcess (txVector, pi) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(newTx)
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatus (oldTx, newTx) {
|
function updateStatus (oldTx, newTx) {
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,11 @@ function checkPing (deviceEvents) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropRepeatsWith (comparator, arr) {
|
function dropRepeatsWith (comparator, arr) {
|
||||||
var fullReduce = _.reduce.convert({cap: false})
|
|
||||||
const iteratee = (acc, val) => val === acc.last
|
const iteratee = (acc, val) => val === acc.last
|
||||||
? acc
|
? acc
|
||||||
: {arr: _.concat(acc.arr, val), last: val}
|
: {arr: _.concat(acc.arr, val), last: val}
|
||||||
|
|
||||||
return fullReduce(iteratee, {arr: []}, arr).arr
|
return _.reduce(iteratee, {arr: []}, arr).arr
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkStuckScreen (deviceEvents) {
|
function checkStuckScreen (deviceEvents) {
|
||||||
|
|
|
||||||
102
lib/plugins.js
102
lib/plugins.js
|
|
@ -15,6 +15,7 @@ const wallet = require('./wallet')
|
||||||
const exchange = require('./exchange')
|
const exchange = require('./exchange')
|
||||||
const sms = require('./sms')
|
const sms = require('./sms')
|
||||||
const email = require('./email')
|
const email = require('./email')
|
||||||
|
const cashOutHelper = require('./cash-out-helper')
|
||||||
|
|
||||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||||
|
|
||||||
|
|
@ -71,47 +72,83 @@ function plugins (settings, deviceId) {
|
||||||
return balances
|
return balances
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCassettes () {
|
function isZeroConf (tx) {
|
||||||
|
const config = configManager.scoped(tx.cryptoCode, deviceId, settings.config)
|
||||||
|
const zeroConfLimit = config.zeroConfLimit
|
||||||
|
return tx.fiat.lte(zeroConfLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeAvailableCassettes (cassettes, redeemableTxs) {
|
||||||
|
if (_.isEmpty(redeemableTxs)) return cassettes
|
||||||
|
|
||||||
|
const sumTxs = (sum, tx) => {
|
||||||
|
const bills = tx.bills
|
||||||
|
const sameDenominations = a => a[0].denomination === a[1].denomination
|
||||||
|
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
|
||||||
|
|
||||||
|
if (!doDenominationsMatch) {
|
||||||
|
throw new Error('Denominations don\'t add up, cassettes were changed.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
|
||||||
|
}
|
||||||
|
|
||||||
|
const provisioned = _.reduce(sumTxs, [0, 0], redeemableTxs)
|
||||||
|
const zipped = _.zip(_.map('count', cassettes), provisioned)
|
||||||
|
const counts = _.map(r => r[0] - r[1], zipped)
|
||||||
|
|
||||||
|
if (_.some(_.lt(_, 0), counts)) {
|
||||||
|
throw new Error('Negative note count: %j', counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
denomination: cassettes[0].denomination,
|
||||||
|
count: counts[0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
denomination: cassettes[1].denomination,
|
||||||
|
count: counts[1]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAvailableCassettes (excludeTxId) {
|
||||||
const config = configManager.machineScoped(deviceId, settings.config)
|
const config = configManager.machineScoped(deviceId, settings.config)
|
||||||
|
|
||||||
if (!config.cashOutEnabled) return Promise.resolve()
|
if (!config.cashOutEnabled) return Promise.resolve()
|
||||||
|
|
||||||
const cassettes = [ config.topCashOutDenomination,
|
const denominations = [ config.topCashOutDenomination,
|
||||||
config.bottomCashOutDenomination ]
|
config.bottomCashOutDenomination ]
|
||||||
const virtualCassettes = [config.virtualCashOutDenomination]
|
const virtualCassettes = [config.virtualCashOutDenomination]
|
||||||
|
|
||||||
return dbm.cassetteCounts(deviceId)
|
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||||
.then(rec => {
|
.then(([rec, _redeemableTxs]) => {
|
||||||
if (argv.cassettes) {
|
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
|
||||||
const counts = argv.cassettes.split(',')
|
|
||||||
|
|
||||||
|
const counts = argv.cassettes
|
||||||
|
? argv.cassettes.split(',')
|
||||||
|
: rec.counts
|
||||||
|
|
||||||
|
const cassettes = [
|
||||||
|
{
|
||||||
|
denomination: parseInt(denominations[0], 10),
|
||||||
|
count: parseInt(counts[0], 10)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
denomination: parseInt(denominations[1], 10),
|
||||||
|
count: parseInt(counts[1], 10)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
try {
|
||||||
return {
|
return {
|
||||||
cassettes: [
|
cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
|
||||||
{
|
|
||||||
denomination: parseInt(cassettes[0], 10),
|
|
||||||
count: parseInt(counts[0], 10)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
denomination: parseInt(cassettes[1], 10),
|
|
||||||
count: parseInt(counts[1], 10)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
virtualCassettes
|
virtualCassettes
|
||||||
}
|
}
|
||||||
}
|
} catch (err) {
|
||||||
|
logger.error(err)
|
||||||
return {
|
return {cassettes, virtualCassettes}
|
||||||
cassettes: [
|
|
||||||
{
|
|
||||||
denomination: parseInt(cassettes[0], 10),
|
|
||||||
count: parseInt(rec.counts[0], 10)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
denomination: parseInt(cassettes[1], 10),
|
|
||||||
count: parseInt(rec.counts[1], 10)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
virtualCassettes
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +175,7 @@ function plugins (settings, deviceId) {
|
||||||
const currentConfigVersionPromise = fetchCurrentConfigVersion()
|
const currentConfigVersionPromise = fetchCurrentConfigVersion()
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
buildCassettes(),
|
buildAvailableCassettes(),
|
||||||
pingPromise,
|
pingPromise,
|
||||||
currentConfigVersionPromise
|
currentConfigVersionPromise
|
||||||
].concat(tickerPromises, balancePromises)
|
].concat(tickerPromises, balancePromises)
|
||||||
|
|
@ -512,6 +549,7 @@ function plugins (settings, deviceId) {
|
||||||
sendCoins,
|
sendCoins,
|
||||||
newAddress,
|
newAddress,
|
||||||
isHd,
|
isHd,
|
||||||
|
isZeroConf,
|
||||||
getStatus,
|
getStatus,
|
||||||
dispenseAck,
|
dispenseAck,
|
||||||
getPhoneCode,
|
getPhoneCode,
|
||||||
|
|
@ -522,7 +560,7 @@ function plugins (settings, deviceId) {
|
||||||
sweepHd,
|
sweepHd,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
checkBalances,
|
checkBalances,
|
||||||
buildCassettes,
|
buildAvailableCassettes,
|
||||||
buy,
|
buy,
|
||||||
sell
|
sell
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
migrations/030-cash-out-provision.js
Normal file
15
migrations/030-cash-out-provision.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
var db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
'alter table cash_out_txs add column provisioned_1 integer',
|
||||||
|
'alter table cash_out_txs add column provisioned_2 integer',
|
||||||
|
'alter table cash_out_txs add column denomination_1 integer',
|
||||||
|
'alter table cash_out_txs add column denomination_2 integer'
|
||||||
|
]
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue