diff --git a/backend/src/graphql/model/Balance.ts b/backend/src/graphql/model/Balance.ts index aaeecd0d7..2f1eeb406 100644 --- a/backend/src/graphql/model/Balance.ts +++ b/backend/src/graphql/model/Balance.ts @@ -17,6 +17,6 @@ export class Balance { @Field(() => Decimal) decay: Decimal - @Field(() => String) - decayDate: string + @Field(() => Date) + decayDate: Date } diff --git a/backend/src/graphql/model/Decay.ts b/backend/src/graphql/model/Decay.ts index a56be6ff3..f1204e730 100644 --- a/backend/src/graphql/model/Decay.ts +++ b/backend/src/graphql/model/Decay.ts @@ -1,6 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { ObjectType, Field } from 'type-graphql' +import { ObjectType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' @ObjectType() @@ -31,6 +29,6 @@ export class Decay { @Field(() => Date, { nullable: true }) end: Date | null - @Field(() => Number, { nullable: true }) + @Field(() => Int, { nullable: true }) duration: number | null } diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index 8d32ec80e..cf2e899b2 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { ObjectType, Field } from 'type-graphql' import { Decay } from './Decay' import { Transaction as dbTransaction } from '@entity/Transaction' @@ -25,7 +23,7 @@ export class Transaction { transaction.decay, transaction.decayStart, transaction.balanceDate, - (transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000, + Math.round((transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000), ) } this.memo = transaction.memo diff --git a/backend/src/graphql/model/TransactionList.ts b/backend/src/graphql/model/TransactionList.ts index d4fcb65eb..2f3d1d080 100644 --- a/backend/src/graphql/model/TransactionList.ts +++ b/backend/src/graphql/model/TransactionList.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { ObjectType, Field } from 'type-graphql' import CONFIG from '../../config' import Decimal from 'decimal.js-light' diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index c23ea0a58..1a187a38f 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { ObjectType, Field } from 'type-graphql' import { KlickTipp } from './KlickTipp' import { User as dbUser } from '@entity/User' diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 2dfaafbcc..b83524e1a 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -307,13 +307,13 @@ export class AdminResolver { const receivedCallDate = new Date() const transactionRepository = getCustomRepository(TransactionRepository) - const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId) + const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId) let newBalance = new Decimal(0) - if (lastUserTransaction) { + if (lastTransaction) { newBalance = calculateDecay( - lastUserTransaction.balance, - lastUserTransaction.balanceDate, + lastTransaction.balance, + lastTransaction.balanceDate, receivedCallDate, ).balance } @@ -367,7 +367,7 @@ async function getUserCreations(ids: number[], includePending = true): Promise= ${dateFilter} ${unionString}) AS result GROUP BY month, userId diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index b06bfd568..14b80a5ea 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -30,7 +30,8 @@ import { RIGHTS } from '../../auth/RIGHTS' import { User } from '../model/User' import { communityUser } from '../../util/communityUser' import { virtualDecayTransaction } from '../../util/virtualDecayTransaction' -// import Decimal from '../scalar/Decimal' +import Decimal from 'decimal.js-light' +import { calculateDecay } from '../../util/decay' @Resolver() export class TransactionResolver { @@ -47,6 +48,7 @@ export class TransactionResolver { }: Paginated, @Ctx() context: any, ): Promise { + const now = new Date() // find user const userRepository = getCustomRepository(UserRepository) // TODO: separate those usecases - this is a security issue @@ -60,65 +62,6 @@ export class TransactionResolver { { order: { balanceDate: 'DESC' } }, ) - if (!lastTransaction) { - // TODO Have proper return type here - throw new Error('User has no transactions') - } - - // find transactions - const limit = currentPage === 1 && order === Order.DESC ? pageSize - 1 : pageSize - const offset = - currentPage === 1 ? 0 : (currentPage - 1) * pageSize - (order === Order.DESC ? 1 : 0) - const transactionRepository = getCustomRepository(TransactionRepository) - const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged( - user.id, - limit, - offset, - order, - onlyCreations, - ) - - // find involved users - let involvedUserIds: number[] = [] - userTransactions.forEach((transaction: dbTransaction) => { - involvedUserIds.push(transaction.userId) - if (transaction.linkedUserId) { - involvedUserIds.push(transaction.linkedUserId) - } - }) - // remove duplicates - involvedUserIds = involvedUserIds.filter((value, index, self) => self.indexOf(value) === index) - // We need to show the name for deleted users for old transactions - const involvedDbUsers = await dbUser - .createQueryBuilder() - .withDeleted() - .where('id IN (:...userIds)', { userIds: involvedUserIds }) - .getMany() - const involvedUsers = involvedDbUsers.map((u) => new User(u)) - - const self = new User(user) - const transactions: Transaction[] = [] - - // decay transaction - if (currentPage === 1 && order === Order.DESC) { - const now = new Date() - transactions.push( - virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self), - ) - } - - // transactions - for (let i = 0; i < userTransactions.length; i++) { - const userTransaction = userTransactions[i] - let linkedUser = null - if (userTransaction.typeId === TransactionTypeId.CREATION) { - linkedUser = communityUser - } else { - linkedUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId) - } - transactions.push(new Transaction(userTransaction, self, linkedUser)) - } - // get GDT let balanceGDT = null try { @@ -134,9 +77,59 @@ export class TransactionResolver { console.log('Could not query GDT Server', err) } + if (!lastTransaction) { + return new TransactionList(new Decimal(0), [], 0, balanceGDT) + } + + // find transactions + // first page can contain 26 due to virtual decay transaction + const offset = (currentPage - 1) * pageSize + const transactionRepository = getCustomRepository(TransactionRepository) + const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged( + user.id, + pageSize, + offset, + order, + onlyCreations, + ) + + // find involved users; I am involved + const involvedUserIds: number[] = [user.id] + userTransactions.forEach((transaction: dbTransaction) => { + if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) { + involvedUserIds.push(transaction.linkedUserId) + } + }) + // We need to show the name for deleted users for old transactions + const involvedDbUsers = await dbUser + .createQueryBuilder() + .withDeleted() + .where('id IN (:...userIds)', { userIds: involvedUserIds }) + .getMany() + const involvedUsers = involvedDbUsers.map((u) => new User(u)) + + const self = new User(user) + const transactions: Transaction[] = [] + + // decay transaction + if (currentPage === 1 && order === Order.DESC) { + transactions.push( + virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self), + ) + } + + // transactions + userTransactions.forEach((userTransaction) => { + const linkedUser = + userTransaction.typeId === TransactionTypeId.CREATION + ? communityUser + : involvedUsers.find((u) => u.id === userTransaction.linkedUserId) + transactions.push(new Transaction(userTransaction, self, linkedUser)) + }) + // Construct Result return new TransactionList( - lastTransaction.balance, + calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance, transactions, userTransactionsCount, balanceGDT, @@ -184,7 +177,7 @@ export class TransactionResolver { transactionSend.memo = memo transactionSend.userId = senderUser.id transactionSend.linkedUserId = recipientUser.id - transactionSend.amount = amount + transactionSend.amount = amount.mul(-1) transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate transactionSend.decay = sendBalance.decay.decay diff --git a/backend/src/util/decay.test.ts b/backend/src/util/decay.test.ts index 1653376c1..f1111fab4 100644 --- a/backend/src/util/decay.test.ts +++ b/backend/src/util/decay.test.ts @@ -7,36 +7,35 @@ describe('utils/decay', () => { it('has base 0.99999997802044727', () => { const amount = new Decimal(1.0) const seconds = 1 - expect(decayFormula(amount, seconds)).toBe(0.99999997802044727) - }) - // Not sure if the following skiped tests make sence!? - it('has negative decay?', async () => { - const amount = new Decimal(1.0) - const seconds = 1 - expect(decayFormula(amount, seconds)).toBe(-0.99999997802044727) + // TODO: toString() was required, we could not compare two decimals + expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012') }) it('has correct backward calculation', async () => { const amount = new Decimal(1.0) const seconds = -1 - expect(decayFormula(amount, seconds)).toBe(1.0000000219795533) + expect(decayFormula(amount, seconds).toString()).toBe('1.000000021964959992727444') }) - // not possible, nodejs hasn't enough accuracy - it('has correct forward calculation', async () => { - const amount = new Decimal(1.0).div(0.99999997802044727) + // we get pretty close, but not exact here, skipping + it.skip('has correct forward calculation', async () => { + const amount = new Decimal(1.0).div( + new Decimal('0.99999997803504048973201202316767079413460520837376'), + ) const seconds = 1 - expect(decayFormula(amount, seconds)).toBe(1.0) + expect(decayFormula(amount, seconds).toString()).toBe('1.0') }) }) - it.skip('has base 0.99999997802044727', async () => { + it('has base 0.99999997802044727', async () => { const now = new Date() now.setSeconds(1) const oneSecondAgo = new Date(now.getTime()) oneSecondAgo.setSeconds(0) - expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now)).toBe(0.99999997802044727) + expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now).balance.toString()).toBe( + '0.999999978035040489732012', + ) }) it('returns input amount when from and to is the same', async () => { const now = new Date() - expect(calculateDecay(new Decimal(100.0), now, now).balance).toBe(100.0) + expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100') }) })