diff --git a/utils/files.ts b/utils/files.ts index 76b47cc0..e1332541 100644 --- a/utils/files.ts +++ b/utils/files.ts @@ -119,20 +119,122 @@ export function streamToCSV ( } } -const getUniquePrices = (tempTxGroup: TransactionsWithPaybuttonsAndPrices[], groupKey: string, currency: SupportedQuotesType): Set => { - const uniquePrices: Set = new Set() - const quoteId = QUOTE_IDS[currency.toUpperCase()] - tempTxGroup - .forEach(tx => { +class TransactionGroupManager { + private tempTxGroups: Record = {} + + 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 { + const uniquePrices: Set = 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, + quoteId: number + ): void { if (uniquePrices.size > 1) { const nonUniquePrices = [...uniquePrices] const txsForPrice: Record = {} 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 { @@ -140,14 +242,48 @@ const getUniquePrices = (tempTxGroup: TransactionsWithPaybuttonsAndPrices[], gro } 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, @@ -173,61 +309,6 @@ const collapsePaymentsPushTx = ( } as TransactionFileData) } -const collapsePaymentsPushTempGroup = ( - groupKey: string, - tempTxGroups: Record, - 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, @@ -237,52 +318,52 @@ export const collapseSmallPayments = ( paybuttonId?: string ): TransactionFileData[] => { const treatedPayments: TransactionFileData[] = [] - const tempTxGroups: Record = {} + 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>((acc, transaction) => { const networkId = transaction.address.networkId