Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 175 additions & 94 deletions utils/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,35 +119,171 @@ export function streamToCSV (
}
}

const getUniquePrices = (tempTxGroup: TransactionsWithPaybuttonsAndPrices[], groupKey: string, currency: SupportedQuotesType): Set<number> => {
const uniquePrices: Set<number> = new Set()
const quoteId = QUOTE_IDS[currency.toUpperCase()]
tempTxGroup
.forEach(tx => {
class TransactionGroupManager {
private tempTxGroups: Record<string, TransactionsWithPaybuttonsAndPrices[]> = {}

constructor(
private currency: SupportedQuotesType,
private timezone: string
) {}

addToGroup(groupKey: string, tx: TransactionsWithPaybuttonsAndPrices): void {
if (this.tempTxGroups[groupKey] === undefined) {
this.tempTxGroups[groupKey] = []
}
this.tempTxGroups[groupKey].push(tx)
}

processAllGroups(treatedPayments: TransactionFileData[]): void {
Object.keys(this.tempTxGroups).forEach(key => {
this.processGroup(key, treatedPayments)
})
}

processGroup(groupKey: string, treatedPayments: TransactionFileData[]): void {
const tempTxGroup = this.tempTxGroups[groupKey]
if (tempTxGroup === undefined || tempTxGroup.length === 0) return

if (tempTxGroup.length === 1) {
this.addSingleTransaction(tempTxGroup[0], groupKey, treatedPayments)
} else {
this.addCollapsedTransactionGroup(tempTxGroup, groupKey, treatedPayments)
}

this.tempTxGroups[groupKey] = []
}

private addSingleTransaction(
tx: TransactionsWithPaybuttonsAndPrices,
groupKey: string,
treatedPayments: TransactionFileData[]
): void {
const { timestamp, hash, address, amount } = tx
const values = getTransactionValue(tx)
const value = Number(values[this.currency])
const rate = tx.prices.find(p => p.price.quoteId === QUOTE_IDS[this.currency.toUpperCase()])!.price.value
const buttonNames = this.extractButtonNamesFromGroupKey(groupKey)

treatedPayments.push({
amount,
value,
date: moment.tz(timestamp * 1000, this.timezone),
transactionId: hash,
rate,
currency: this.currency,
address: address.address,
notes: buttonNames,
newtworkId: address.networkId
} as TransactionFileData)
}

private addCollapsedTransactionGroup(
tempTxGroup: TransactionsWithPaybuttonsAndPrices[],
groupKey: string,
treatedPayments: TransactionFileData[]
): void {
const totalAmount = tempTxGroup.reduce((sum, p) => sum + Number(p.amount), 0)
const totalValue = tempTxGroup.reduce((sum, p) => sum + Number(getTransactionValue(p)[this.currency]), 0)
const uniquePrices = this.getUniquePrices(tempTxGroup, groupKey)
const rate = new Prisma.Decimal(uniquePrices.values().next().value as number)
const buttonNames = this.extractButtonNamesFromGroupKey(groupKey)
const notes = `${buttonNames} - ${tempTxGroup.length.toString()} transactions`

treatedPayments.push({
amount: totalAmount,
value: totalValue,
date: moment.tz(tempTxGroup[0].timestamp * 1000, this.timezone),
transactionId: DEFAULT_MULTI_VALUES_LINE_LABEL,
rate,
currency: this.currency,
address: DEFAULT_MULTI_VALUES_LINE_LABEL,
newtworkId: tempTxGroup[0].address.networkId,
notes
} as TransactionFileData)
}

private extractButtonNamesFromGroupKey(groupKey: string): string {
return groupKey.split('_').slice(2).join(';')
}

private getUniquePrices(tempTxGroup: TransactionsWithPaybuttonsAndPrices[], groupKey: string): Set<number> {
const uniquePrices: Set<number> = new Set()
const quoteId = QUOTE_IDS[this.currency.toUpperCase()]

tempTxGroup.forEach(tx => {
const price = tx.prices.find(p => p.price.quoteId === quoteId)!.price.value
uniquePrices.add(Number(price))
})
if (uniquePrices.size !== 1) {

if (uniquePrices.size !== 1) {
this.handlePriceValidationError(tempTxGroup, groupKey, uniquePrices, quoteId)
}

return uniquePrices
}

private handlePriceValidationError(
tempTxGroup: TransactionsWithPaybuttonsAndPrices[],
groupKey: string,
uniquePrices: Set<number>,
quoteId: number
): void {
if (uniquePrices.size > 1) {
const nonUniquePrices = [...uniquePrices]
const txsForPrice: Record<number, string[]> = {}
nonUniquePrices.forEach(nonUniquePrice => {
txsForPrice[nonUniquePrice] = tempTxGroup.filter(tx => nonUniquePrice === tx.prices.find(p => p.price.quoteId === quoteId)!.price.value.toNumber()).map(tx => tx.id)
txsForPrice[nonUniquePrice] = tempTxGroup
.filter(tx => nonUniquePrice === tx.prices.find(p => p.price.quoteId === quoteId)!.price.value.toNumber())
.map(tx => tx.id)
})
console.error('ERROR WHEN TRYING TO COLLAPSE TXS INTO DIFFERENT PRICES:', { txsForPrice, nonUniquePrices })
} else {
console.error('ERROR WHEN TRYING TO COLLAPSE TXS INTO DIFFERENT PRICES, NO PRICES FOR GROUP KEY', { groupKey })
}

throw new Error(
RESPONSE_MESSAGES
.INVALID_PRICES_AMOUNT_FOR_TX_ON_CSV_CREATION_500(tempTxGroup.length).message
RESPONSE_MESSAGES.INVALID_PRICES_AMOUNT_FOR_TX_ON_CSV_CREATION_500(tempTxGroup.length).message
)
}
return uniquePrices
}

const collapsePaymentsPushTx = (
const generateGroupKey = (
tx: TransactionsWithPaybuttonsAndPrices,
timezone: string,
userId: string,
paybuttonId?: string
): string => {
const { timestamp } = tx
const dateKey = moment.tz(timestamp * 1000, timezone).format('YYYY-MM-DD')
const dateKeyUTC = moment.utc(timestamp * 1000).format('YYYY-MM-DD')
const buttonNamesKey = extractPaybuttonNames(tx, userId, paybuttonId)

return `${dateKey}_${dateKeyUTC}_${buttonNamesKey}`
}

const extractPaybuttonNames = (
tx: TransactionsWithPaybuttonsAndPrices,
userId: string,
paybuttonId?: string
): string => {
const uniqueButtonNames = new Set(
tx.address.paybuttons
.filter(pb => pb.paybutton.providerUserId === userId)
.map(pb => pb.paybutton.name)
)

if (uniqueButtonNames.size > 1) {
if (paybuttonId !== undefined) {
return tx.address.paybuttons.find(pb => pb.paybutton.id === paybuttonId)?.paybutton.name ?? ''
} else {
return [...uniqueButtonNames].join('_')
}
} else {
return uniqueButtonNames.values().next().value ?? ''
}
}

const addSingleTransactionToResults = (
tx: TransactionsWithPaybuttonsAndPrices,
groupKey: string,
currency: SupportedQuotesType,
Expand All @@ -173,61 +309,6 @@ const collapsePaymentsPushTx = (
} as TransactionFileData)
}

const collapsePaymentsPushTempGroup = (
groupKey: string,
tempTxGroups: Record<string, TransactionsWithPaybuttonsAndPrices[]>,
currency: SupportedQuotesType,
treatedPayments: TransactionFileData[],
timezone: string
): void => {
const tempTxGroup = tempTxGroups[groupKey]
if (tempTxGroup === undefined || tempTxGroup.length === 0) return
if (tempTxGroup.length === 1) {
collapsePaymentsPushTx(tempTxGroup[0], groupKey, currency, treatedPayments, timezone)
tempTxGroups[groupKey] = []
return
}
const totalAmount = tempTxGroup.reduce((sum, p) => sum + Number(p.amount), 0)
const totalValue = tempTxGroup.reduce((sum, p) => sum + Number(getTransactionValue(p)[currency]), 0)
const uniquePrices = getUniquePrices(tempTxGroup, groupKey, currency)
const rate = new Prisma.Decimal(uniquePrices.values().next().value as number)
const buttonNames = groupKey.split('_').slice(2).join(';')
const notes = `${buttonNames} - ${tempTxGroup.length.toString()} transactions`

treatedPayments.push({
amount: totalAmount,
value: totalValue,
date: moment.tz(tempTxGroup[0].timestamp * 1000, timezone),
transactionId: DEFAULT_MULTI_VALUES_LINE_LABEL,
rate,
currency,
address: DEFAULT_MULTI_VALUES_LINE_LABEL,
newtworkId: tempTxGroup[0].address.networkId,
notes
} as TransactionFileData)

tempTxGroups[groupKey] = []
}

const getButtonNames = (tx: TransactionsWithPaybuttonsAndPrices, userId: string, paybuttonId?: string): string => {
let buttonNamesKey: string = ''
const uniqueButtonNames = new Set(
tx.address.paybuttons
.filter(pb => pb.paybutton.providerUserId === userId)
.map(pb => pb.paybutton.name)
)
if (uniqueButtonNames.size > 1) {
if (paybuttonId !== undefined) {
buttonNamesKey = tx.address.paybuttons.find(pb => pb.paybutton.id === paybuttonId)?.paybutton.name ?? ''
} else {
buttonNamesKey = [...uniqueButtonNames].join('_')
}
} else {
buttonNamesKey = uniqueButtonNames.values().next().value ?? ''
}
return buttonNamesKey
}

export const collapseSmallPayments = (
payments: TransactionsWithPaybuttonsAndPrices[],
currency: SupportedQuotesType,
Expand All @@ -237,52 +318,52 @@ export const collapseSmallPayments = (
paybuttonId?: string
): TransactionFileData[] => {
const treatedPayments: TransactionFileData[] = []
const tempTxGroups: Record<string, TransactionsWithPaybuttonsAndPrices[]> = {}
const groupManager = new TransactionGroupManager(currency, timezone)

payments.forEach((tx: TransactionsWithPaybuttonsAndPrices, index: number) => {
const { timestamp } = tx
const values = getTransactionValue(tx)
const value = Number(values[currency])
const dateKey = moment.tz(timestamp * 1000, timezone).format('YYYY-MM-DD')
const dateKeyUTC = moment.utc(timestamp * 1000).format('YYYY-MM-DD')
const buttonNamesKey = getButtonNames(tx, userId, paybuttonId)

const groupKey = `${dateKey}_${dateKeyUTC}_${buttonNamesKey}`

let nextGroupKey: string | null = ''
const nextPayment = payments[index + 1]
if (nextPayment !== undefined) {
const nextDateKey = moment.tz(nextPayment.timestamp * 1000, timezone).format('YYYY-MM-DD')
const nextDateKeyUTC = moment.utc(nextPayment.timestamp * 1000).format('YYYY-MM-DD')
const nextButtonName = getButtonNames(nextPayment, userId, paybuttonId)
const groupKey = generateGroupKey(tx, timezone, userId, paybuttonId)

nextGroupKey = `${nextDateKey}_${nextDateKeyUTC}_${nextButtonName}`
} else {
nextGroupKey = null
}
const shouldProcessGroups = shouldProcessGroupsAtCurrentIndex(
payments, index, timezone, userId, paybuttonId, groupKey
)

if (value < collapseThreshold) {
if (tempTxGroups[groupKey] === undefined) tempTxGroups[groupKey] = []
tempTxGroups[groupKey].push(tx)
groupManager.addToGroup(groupKey, tx)
} else {
Object.keys(tempTxGroups).forEach(key => {
collapsePaymentsPushTempGroup(key, tempTxGroups, currency, treatedPayments, timezone)
})
collapsePaymentsPushTx(tx, groupKey, currency, treatedPayments, timezone)
groupManager.processAllGroups(treatedPayments)
addSingleTransactionToResults(tx, groupKey, currency, treatedPayments, timezone)
}

if (nextGroupKey !== groupKey) {
collapsePaymentsPushTempGroup(groupKey, tempTxGroups, currency, treatedPayments, timezone)
if (shouldProcessGroups) {
groupManager.processGroup(groupKey, treatedPayments)
}
})

Object.keys(tempTxGroups).forEach(key => {
collapsePaymentsPushTempGroup(key, tempTxGroups, currency, treatedPayments, timezone)
})
groupManager.processAllGroups(treatedPayments)

return treatedPayments
}

const shouldProcessGroupsAtCurrentIndex = (
payments: TransactionsWithPaybuttonsAndPrices[],
currentIndex: number,
timezone: string,
userId: string,
paybuttonId: string | undefined,
currentGroupKey: string
): boolean => {
const nextPayment = payments[currentIndex + 1]
if (nextPayment === undefined) {
return false // No next payment, will be handled at the end
}

const nextGroupKey = generateGroupKey(nextPayment, timezone, userId, paybuttonId)
return nextGroupKey !== currentGroupKey
}

const sortPaymentsByNetworkId = (payments: TransactionsWithPaybuttonsAndPrices[]): TransactionsWithPaybuttonsAndPrices[] => {
const groupedByNetworkIdPayments = payments.reduce<Record<number, TransactionsWithPaybuttonsAndPrices[]>>((acc, transaction) => {
const networkId = transaction.address.networkId
Expand Down