Skip to content

Commit d0598a0

Browse files
authored
Merge pull request #982 from PayButton/feat/refactor-collapse-txs-csv
Feat/refactor collapse txs csv
2 parents 0ea70ac + c9376b2 commit d0598a0

File tree

4 files changed

+143
-96
lines changed

4 files changed

+143
-96
lines changed

pages/api/paybutton/download/transactions/[paybuttonId].ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default async (req: any, res: any): Promise<void> => {
5252
};
5353
const transactions = await fetchTransactionsByPaybuttonId(paybutton.id, networkIdArray)
5454
res.setHeader('Content-Type', 'text/csv')
55-
await downloadTxsFile(res, quoteSlug, timezone, transactions)
55+
await downloadTxsFile(res, quoteSlug, timezone, transactions, userId, paybuttonId)
5656
} catch (error: any) {
5757
switch (error.message) {
5858
case RESPONSE_MESSAGES.PAYBUTTON_ID_NOT_PROVIDED_400.message:

pages/api/payments/download/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default async (req: any, res: any): Promise<void> => {
4444
networkIdArray = [networkId]
4545
};
4646
const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray)
47-
await downloadTxsFile(res, quoteSlug, timezone, transactions)
47+
await downloadTxsFile(res, quoteSlug, timezone, transactions, userId)
4848
} catch (error: any) {
4949
switch (error.message) {
5050
case RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.message:

tests/unittests/utils/files.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,20 @@ describe('collapseSmallPayments', () => {
192192

193193

194194
it('should collapse small payments correctly', () => {
195-
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 1);
195+
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 1, 'dev2-uid');
196196

197197
expect(result).toHaveLength(3);
198198
});
199199

200200
it('should collapse small payments threshold 2 USD', () => {
201-
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 2);
201+
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 2, 'dev2-uid');
202202

203203
expect(result).toHaveLength(1);
204204
});
205205

206206

207207
it('amount should be the sum of colapsed tx amounts', () => {
208-
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 1);
208+
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 1, 'dev2-uid');
209209
const sumOfSmallPaymentsAmount = Number(mockedSmallerThen1UsdPayments.reduce((sum, payment) => sum.plus(payment.amount), new Decimal(0)));
210210

211211
const collapsedPayment = result[1];
@@ -214,7 +214,7 @@ describe('collapseSmallPayments', () => {
214214
});
215215

216216
it('value should be the sum of colapsed tx values - USD', () => {
217-
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 1);
217+
const result = collapseSmallPayments(mockedPayments, currencyUsd, timezone, 1, 'dev2-uid');
218218
const sumOfSmallPaymentsAmount = Number(mockedSmallerThen1UsdPayments.reduce((sum, payment) => sum.plus(Number(getTransactionValue(payment)[currencyUsd])), new Decimal(0)));
219219

220220
const collapsedPayment = result[1];
@@ -223,7 +223,7 @@ describe('collapseSmallPayments', () => {
223223
});
224224

225225
it('value should be the sum of colapsed tx values - CAD', () => {
226-
const result = collapseSmallPayments(mockedPayments, currencyCad, timezone, 1);
226+
const result = collapseSmallPayments(mockedPayments, currencyCad, timezone, 1, 'dev2-uid');
227227
const sumOfSmallPaymentsAmount = Number(mockedSmallerThen1UsdPayments.reduce((sum, payment) => sum.plus(Number(getTransactionValue(payment)[currencyCad])), new Decimal(0)));
228228

229229
const collapsedPayment = result[1];

utils/files.ts

Lines changed: 136 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -119,122 +119,166 @@ export function streamToCSV (
119119
}
120120
}
121121

122-
export const collapseSmallPayments = (
123-
payments: TransactionsWithPaybuttonsAndPrices[],
124-
currency: SupportedQuotesType,
125-
timezone: string,
126-
collapseThreshold: number
127-
): TransactionFileData[] => {
128-
const treatedPayments: TransactionFileData[] = []
129-
const tempTxGroups: Record<string, TransactionsWithPaybuttonsAndPrices[]> = {}
130-
let totalPaymentsTreated = 0
131-
132-
const pushTempGroup = (groupKey: string): void => {
133-
const tempTxGroup = tempTxGroups[groupKey]
134-
if (tempTxGroup === undefined || tempTxGroup.length === 0) return
135-
if (tempTxGroup.length === 1) {
136-
pushTx(tempTxGroup[0])
137-
tempTxGroups[groupKey] = []
138-
return
139-
}
140-
const totalAmount = tempTxGroup.reduce((sum, p) => sum + Number(p.amount), 0)
141-
const totalValue = tempTxGroup.reduce((sum, p) => sum + Number(getTransactionValue(p)[currency]), 0)
142-
const uniquePrices: Set<number> = new Set()
143-
const quoteId = QUOTE_IDS[currency.toUpperCase()]
144-
tempTxGroup
145-
.forEach(tx => {
146-
const price = tx.prices.find(p => p.price.quoteId === quoteId)!.price.value
147-
uniquePrices.add(Number(price))
122+
const getUniquePrices = (tempTxGroup: TransactionsWithPaybuttonsAndPrices[], groupKey: string, currency: SupportedQuotesType): Set<number> => {
123+
const uniquePrices: Set<number> = new Set()
124+
const quoteId = QUOTE_IDS[currency.toUpperCase()]
125+
tempTxGroup
126+
.forEach(tx => {
127+
const price = tx.prices.find(p => p.price.quoteId === quoteId)!.price.value
128+
uniquePrices.add(Number(price))
129+
})
130+
if (uniquePrices.size !== 1) {
131+
if (uniquePrices.size > 1) {
132+
const nonUniquePrices = [...uniquePrices]
133+
const txsForPrice: Record<number, string[]> = {}
134+
nonUniquePrices.forEach(nonUniquePrice => {
135+
txsForPrice[nonUniquePrice] = tempTxGroup.filter(tx => nonUniquePrice === tx.prices.find(p => p.price.quoteId === quoteId)!.price.value.toNumber()).map(tx => tx.id)
148136
})
149-
if (uniquePrices.size !== 1) {
150-
if (uniquePrices.size > 1) {
151-
const nonUniquePrices = [...uniquePrices]
152-
const txsForPrice: Record<number, string[]> = {}
153-
nonUniquePrices.forEach(nonUniquePrice => {
154-
txsForPrice[nonUniquePrice] = tempTxGroup.filter(tx => nonUniquePrice === tx.prices.find(p => p.price.quoteId === quoteId)!.price.value.toNumber()).map(tx => tx.id)
155-
})
156-
console.error('ERROR WHEN TRYING TO COLLAPSE TXS INTO DIFFERENT PRICES:', { txsForPrice, nonUniquePrices })
157-
} else {
158-
console.error('ERROR WHEN TRYING TO COLLAPSE TXS INTO DIFFERENT PRICES, NO PRICES FOR GROUP KEY', { groupKey })
159-
}
160-
161-
throw new Error(
162-
RESPONSE_MESSAGES
163-
.INVALID_PRICES_AMOUNT_FOR_TX_ON_CSV_CREATION_500(tempTxGroup.length).message
164-
)
137+
console.error('ERROR WHEN TRYING TO COLLAPSE TXS INTO DIFFERENT PRICES:', { txsForPrice, nonUniquePrices })
138+
} else {
139+
console.error('ERROR WHEN TRYING TO COLLAPSE TXS INTO DIFFERENT PRICES, NO PRICES FOR GROUP KEY', { groupKey })
165140
}
166-
const rate = new Prisma.Decimal(uniquePrices.values().next().value as number)
167-
const buttonName = tempTxGroup[0].address.paybuttons[0].paybutton.name
168-
const notes = `${buttonName} - ${tempTxGroup.length.toString()} transactions`
169141

170-
totalPaymentsTreated += tempTxGroup.length
142+
throw new Error(
143+
RESPONSE_MESSAGES
144+
.INVALID_PRICES_AMOUNT_FOR_TX_ON_CSV_CREATION_500(tempTxGroup.length).message
145+
)
146+
}
147+
return uniquePrices
148+
}
171149

172-
treatedPayments.push({
173-
amount: totalAmount,
174-
value: totalValue,
175-
date: moment.tz(tempTxGroup[0].timestamp * 1000, timezone),
176-
transactionId: DEFAULT_MULTI_VALUES_LINE_LABEL,
177-
rate,
178-
currency,
179-
address: DEFAULT_MULTI_VALUES_LINE_LABEL,
180-
newtworkId: tempTxGroup[0].address.networkId,
181-
notes
182-
} as TransactionFileData)
150+
const collapsePaymentsPushTx = (
151+
tx: TransactionsWithPaybuttonsAndPrices,
152+
groupKey: string,
153+
currency: SupportedQuotesType,
154+
treatedPayments: TransactionFileData[],
155+
timezone: string
156+
): void => {
157+
const { timestamp, hash, address, amount } = tx
158+
const values = getTransactionValue(tx)
159+
const value = Number(values[currency])
160+
const rate = tx.prices.find(p => p.price.quoteId === QUOTE_IDS[currency.toUpperCase()])!.price.value
161+
const buttonNames = groupKey.split('_').slice(2).join(';')
162+
163+
treatedPayments.push({
164+
amount,
165+
value,
166+
date: moment.tz(timestamp * 1000, timezone),
167+
transactionId: hash,
168+
rate,
169+
currency,
170+
address: address.address,
171+
notes: buttonNames,
172+
newtworkId: address.networkId
173+
} as TransactionFileData)
174+
}
183175

176+
const collapsePaymentsPushTempGroup = (
177+
groupKey: string,
178+
tempTxGroups: Record<string, TransactionsWithPaybuttonsAndPrices[]>,
179+
currency: SupportedQuotesType,
180+
treatedPayments: TransactionFileData[],
181+
timezone: string
182+
): void => {
183+
const tempTxGroup = tempTxGroups[groupKey]
184+
if (tempTxGroup === undefined || tempTxGroup.length === 0) return
185+
if (tempTxGroup.length === 1) {
186+
collapsePaymentsPushTx(tempTxGroup[0], groupKey, currency, treatedPayments, timezone)
184187
tempTxGroups[groupKey] = []
188+
return
185189
}
190+
const totalAmount = tempTxGroup.reduce((sum, p) => sum + Number(p.amount), 0)
191+
const totalValue = tempTxGroup.reduce((sum, p) => sum + Number(getTransactionValue(p)[currency]), 0)
192+
const uniquePrices = getUniquePrices(tempTxGroup, groupKey, currency)
193+
const rate = new Prisma.Decimal(uniquePrices.values().next().value as number)
194+
const buttonNames = groupKey.split('_').slice(2).join(';')
195+
const notes = `${buttonNames} - ${tempTxGroup.length.toString()} transactions`
196+
197+
treatedPayments.push({
198+
amount: totalAmount,
199+
value: totalValue,
200+
date: moment.tz(tempTxGroup[0].timestamp * 1000, timezone),
201+
transactionId: DEFAULT_MULTI_VALUES_LINE_LABEL,
202+
rate,
203+
currency,
204+
address: DEFAULT_MULTI_VALUES_LINE_LABEL,
205+
newtworkId: tempTxGroup[0].address.networkId,
206+
notes
207+
} as TransactionFileData)
186208

187-
const pushTx = (tx: TransactionsWithPaybuttonsAndPrices): void => {
188-
const { timestamp, hash, address, amount } = tx
189-
const values = getTransactionValue(tx)
190-
const value = Number(values[currency])
191-
const rate = tx.prices.find(p => p.price.quoteId === QUOTE_IDS[currency.toUpperCase()])!.price.value
209+
tempTxGroups[groupKey] = []
210+
}
192211

193-
treatedPayments.push({
194-
amount,
195-
value,
196-
date: moment.tz(timestamp * 1000, timezone),
197-
transactionId: hash,
198-
rate,
199-
currency,
200-
address: address.address,
201-
notes: '',
202-
newtworkId: address.networkId
203-
} as TransactionFileData)
204-
totalPaymentsTreated += 1
212+
const getButtonNames = (tx: TransactionsWithPaybuttonsAndPrices, userId: string, paybuttonId?: string): string => {
213+
let buttonNamesKey: string = ''
214+
const uniqueButtonNames = new Set(
215+
tx.address.paybuttons
216+
.filter(pb => pb.paybutton.providerUserId === userId)
217+
.map(pb => pb.paybutton.name)
218+
)
219+
if (uniqueButtonNames.size > 1) {
220+
if (paybuttonId !== undefined) {
221+
buttonNamesKey = tx.address.paybuttons.find(pb => pb.paybutton.id === paybuttonId)?.paybutton.name ?? ''
222+
} else {
223+
buttonNamesKey = [...uniqueButtonNames].join('_')
224+
}
225+
} else {
226+
buttonNamesKey = uniqueButtonNames.values().next().value ?? ''
205227
}
228+
return buttonNamesKey
229+
}
230+
231+
export const collapseSmallPayments = (
232+
payments: TransactionsWithPaybuttonsAndPrices[],
233+
currency: SupportedQuotesType,
234+
timezone: string,
235+
collapseThreshold: number,
236+
userId: string,
237+
paybuttonId?: string
238+
): TransactionFileData[] => {
239+
const treatedPayments: TransactionFileData[] = []
240+
const tempTxGroups: Record<string, TransactionsWithPaybuttonsAndPrices[]> = {}
206241

207242
payments.forEach((tx: TransactionsWithPaybuttonsAndPrices, index: number) => {
208243
const { timestamp } = tx
209244
const values = getTransactionValue(tx)
210245
const value = Number(values[currency])
211246
const dateKey = moment.tz(timestamp * 1000, timezone).format('YYYY-MM-DD')
212247
const dateKeyUTC = moment.utc(timestamp * 1000).format('YYYY-MM-DD')
213-
const groupKey = `${dateKey}_${dateKeyUTC}`
248+
const buttonNamesKey = getButtonNames(tx, userId, paybuttonId)
249+
250+
const groupKey = `${dateKey}_${dateKeyUTC}_${buttonNamesKey}`
214251

252+
let nextGroupKey: string | null = ''
215253
const nextPayment = payments[index + 1]
216-
const nextDateKey = nextPayment === undefined ? null : moment.tz(nextPayment.timestamp * 1000, timezone).format('YYYY-MM-DD')
217-
const nextDateKeyUTC = nextPayment === undefined ? null : moment.utc(nextPayment.timestamp * 1000).format('YYYY-MM-DD')
218-
const nextGroupKey = nextDateKey === null || nextDateKeyUTC === null ? null : `${nextDateKey}_${nextDateKeyUTC}`
254+
if (nextPayment !== undefined) {
255+
const nextDateKey = moment.tz(nextPayment.timestamp * 1000, timezone).format('YYYY-MM-DD')
256+
const nextDateKeyUTC = moment.utc(nextPayment.timestamp * 1000).format('YYYY-MM-DD')
257+
const nextButtonName = getButtonNames(nextPayment, userId, paybuttonId)
258+
259+
nextGroupKey = `${nextDateKey}_${nextDateKeyUTC}_${nextButtonName}`
260+
} else {
261+
nextGroupKey = null
262+
}
219263

220264
if (value < collapseThreshold) {
221265
if (tempTxGroups[groupKey] === undefined) tempTxGroups[groupKey] = []
222266
tempTxGroups[groupKey].push(tx)
223267
} else {
224-
Object.keys(tempTxGroups).forEach(pushTempGroup)
225-
pushTx(tx)
268+
Object.keys(tempTxGroups).forEach(key => {
269+
collapsePaymentsPushTempGroup(key, tempTxGroups, currency, treatedPayments, timezone)
270+
})
271+
collapsePaymentsPushTx(tx, groupKey, currency, treatedPayments, timezone)
226272
}
227273

228274
if (nextGroupKey !== groupKey) {
229-
pushTempGroup(groupKey)
275+
collapsePaymentsPushTempGroup(groupKey, tempTxGroups, currency, treatedPayments, timezone)
230276
}
231277
})
232278

233-
Object.keys(tempTxGroups).forEach(pushTempGroup)
234-
235-
if (totalPaymentsTreated !== payments.length) {
236-
throw new Error('Error to collapse payments')
237-
}
279+
Object.keys(tempTxGroups).forEach(key => {
280+
collapsePaymentsPushTempGroup(key, tempTxGroups, currency, treatedPayments, timezone)
281+
})
238282

239283
return treatedPayments
240284
}
@@ -282,14 +326,17 @@ export const downloadTxsFile = async (
282326
currency: SupportedQuotesType,
283327
timezone: string,
284328
transactions: TransactionsWithPaybuttonsAndPrices[],
329+
userId: string,
330+
paybuttonId?: string,
285331
collapseTransactions: boolean = true,
286-
collapseThreshold: number = DEFAULT_CSV_COLLAPSE_THRESHOLD): Promise<void> => {
332+
collapseThreshold: number = DEFAULT_CSV_COLLAPSE_THRESHOLD
333+
): Promise<void> => {
287334
const sortedPayments = sortPaymentsByNetworkId(transactions)
288335
let treatedPayments: TransactionFileData[] = []
289336
if (collapseTransactions) {
290-
treatedPayments = collapseSmallPayments(sortedPayments, currency, timezone, collapseThreshold)
337+
treatedPayments = collapseSmallPayments(sortedPayments, currency, timezone, collapseThreshold, userId, paybuttonId)
291338
} else {
292-
treatedPayments = getPaybuttonTransactionsFileData(transactions, currency, timezone)
339+
treatedPayments = getPaybuttonTransactionsFileData(sortedPayments, currency, timezone)
293340
}
294341
const mappedPaymentsData = treatedPayments.map(payment => formatPaybuttonTransactionsFileData(payment))
295342

0 commit comments

Comments
 (0)