lamassu-server/packages/server/lib/new-admin/services/transactions.js
2025-06-19 11:23:12 +01:00

252 lines
6 KiB
JavaScript

const _ = require('lodash/fp')
const db = require('../../db')
const BN = require('../../bn')
const { utils: coinUtils } = require('@lamassu/coins')
const cashInTx = require('../../cash-in/cash-in-tx')
const { REDEEMABLE_AGE } = require('../../cash-out/cash-out-helper')
const {
transactions: { getTransactionList },
} = require('typesafe-db')
function addProfits(txs) {
return _.map(
it => ({
...it,
profit: getProfit(it).toString(),
cryptoAmount: getCryptoAmount(it).toString(),
}),
txs,
)
}
function batch({
from = new Date(0).toISOString(),
until = new Date().toISOString(),
limit = null,
offset = 0,
txClass = null,
deviceId = null,
customerId = null,
cryptoCode = null,
toAddress = null,
status = null,
swept = null,
excludeTestingCustomers = false,
simplified,
}) {
const isCsvExport = _.isBoolean(simplified)
return (
Promise.all([
getTransactionList(
{
from,
until,
cryptoCode,
txClass,
deviceId,
toAddress,
customerId,
swept,
status,
excludeTestingCustomers,
},
{ limit, offset },
),
])
// Promise.all(promises)
.then(it => addProfits(it[0]))
.then(res =>
!isCsvExport
? res
: // GQL transactions and transactionsCsv both use this function and
// if we don't check for the correct simplified value, the Transactions page polling
// will continuously build a csv in the background
simplified
? simplifiedBatch(res)
: advancedBatch(res),
)
)
}
function advancedBatch(data) {
const fields = [
'txClass',
'id',
'deviceId',
'toAddress',
'cryptoAtoms',
'cryptoCode',
'fiat',
'fiatCode',
'fee',
'status',
'profit',
'cryptoAmount',
'dispense',
'notified',
'redeem',
'phone',
'error',
'fixedFee',
'created',
'confirmedAt',
'hdIndex',
'swept',
'timedout',
'dispenseConfirmed',
'provisioned1',
'provisioned2',
'provisioned3',
'provisioned4',
'provisionedRecycler1',
'provisionedRecycler2',
'provisionedRecycler3',
'provisionedRecycler4',
'provisionedRecycler5',
'provisionedRecycler6',
'denomination1',
'denomination2',
'denomination3',
'denomination4',
'denominationRecycler1',
'denominationRecycler2',
'denominationRecycler3',
'denominationRecycler4',
'denominationRecycler5',
'denominationRecycler6',
'errorCode',
'customerId',
'txVersion',
'publishedAt',
'termsAccepted',
'layer2Address',
'commissionPercentage',
'rawTickerPrice',
'receivedCryptoAtoms',
'discount',
'txHash',
'customerPhone',
'customerEmail',
'customerIdCardDataNumber',
'customerIdCardDataExpiration',
'customerIdCardData',
'customerName',
'sendTime',
'customerFrontCameraPath',
'customerIdCardPhotoPath',
'expired',
'machineName',
'walletScore',
]
const addAdvancedFields = _.map(it => ({
...it,
fixedFee: it.fixedFee ?? null,
fee: it.fee ?? null,
}))
return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data)
}
function simplifiedBatch(data) {
const fields = [
'txClass',
'id',
'created',
'machineName',
'fee',
'cryptoCode',
'cryptoAtoms',
'fiat',
'fiatCode',
'phone',
'email',
'toAddress',
'txHash',
'dispense',
'error',
'status',
'profit',
'cryptoAmount',
]
return _.map(_.pick(fields))(data)
}
const getCryptoAmount = it =>
coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode)
const getProfit = it => {
/* fiat - crypto*tickerPrice */
const calcCashInProfit = (fiat, crypto, tickerPrice) =>
fiat.minus(crypto.times(tickerPrice))
/* crypto*tickerPrice - fiat */
const calcCashOutProfit = (fiat, crypto, tickerPrice) =>
crypto.times(tickerPrice).minus(fiat)
const fiat = BN(it.fiat)
const crypto = getCryptoAmount(it)
const tickerPrice = BN(it.rawTickerPrice)
const isCashIn = it.txClass === 'cashIn'
return isCashIn
? calcCashInProfit(fiat, crypto, tickerPrice)
: calcCashOutProfit(fiat, crypto, tickerPrice)
}
function getTx(txId, txClass) {
const cashInSql = `select 'cashIn' as tx_class, txs.*,
((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired
from cash_in_txs as txs
where txs.id=$2`
const cashOutSql = `select 'cashOut' as tx_class,
txs.*,
(extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $2 as expired
from cash_out_txs txs
where txs.id=$1`
return txClass === 'cashIn'
? db.oneOrNone(cashInSql, [cashInTx.PENDING_INTERVAL, txId])
: db.oneOrNone(cashOutSql, [txId, REDEEMABLE_AGE])
}
function getTxAssociatedData(txId, txClass) {
const billsSql = `select 'bills' as bills, b.* from bills b where cash_in_txs_id = $1`
const actionsSql = `select 'cash_out_actions' as cash_out_actions, actions.* from cash_out_actions actions where tx_id = $1`
return txClass === 'cashIn'
? db.manyOrNone(billsSql, [txId])
: db.manyOrNone(actionsSql, [txId])
}
function updateTxCustomerPhoto(customerId, txId, direction, data) {
const formattedData = _.mapKeys(_.snakeCase, data)
const cashInSql =
'UPDATE cash_in_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4'
const cashOutSql =
'UPDATE cash_out_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4'
return direction === 'cashIn'
? db.oneOrNone(cashInSql, [
formattedData.tx_customer_photo_at,
formattedData.tx_customer_photo_path,
customerId,
txId,
])
: db.oneOrNone(cashOutSql, [
formattedData.tx_customer_photo_at,
formattedData.tx_customer_photo_path,
customerId,
txId,
])
}
module.exports = {
batch,
getTx,
getTxAssociatedData,
updateTxCustomerPhoto,
}