diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 4724956b4..4ba5dcd0b 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -74,10 +74,7 @@ export class TransactionLinkResolver { const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) // validate amount - const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) - if (!sendBalance) { - throw new Error("user hasn't enough GDD or amount is < 0") - } + await calculateBalance(user.id, holdAvailableAmount, createdDate) const transactionLink = dbTransactionLink.create() transactionLink.userId = user.id diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index b3ad36fcd..d391f8ab9 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -16,7 +16,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { EventProtocol } from '@entity/EventProtocol' import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' -import { cleanDB, testEnvironment } from '@test/helpers' +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' import { GraphQLError } from 'graphql' import { findUserByEmail } from './UserResolver' @@ -246,21 +246,49 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], + errors: [new GraphQLError(`User has not received any GDD yet`)], }), ) }) it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - `user hasn't enough GDD or amount is < 0 : balance=null`, + `No prior transaction found for user with id: ${user[1].id}`, ) }) }) + + describe('sending negative amount', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: -50, + memo: 'testing negative', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Transaction amount must be greater than 0')], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Transaction amount must be greater than 0: -50') + }) + }) }) describe('user has some GDD', () => { beforeAll(async () => { + resetToken() + + // login as bob again + await query({ mutation: login, variables: bobData }) + // create contribution as user bob const contribution = await mutate({ mutation: createContribution, @@ -280,35 +308,6 @@ describe('send coins', () => { await query({ mutation: login, variables: bobData }) }) - afterAll(async () => { - await cleanDB() - }) - - describe('trying to send negative amount', () => { - it('throws an error', async () => { - expect( - await mutate({ - mutation: sendCoins, - variables: { - email: 'peter@lustig.de', - amount: -50, - memo: 'testing negative', - }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - `user hasn't enough GDD or amount is < 0 : balance=null`, - ) - }) - }) - describe('good transaction', () => { it('sends the coins', async () => { expect( @@ -317,7 +316,7 @@ describe('send coins', () => { variables: { email: 'peter@lustig.de', amount: 50, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }, }), ).toEqual( @@ -333,7 +332,7 @@ describe('send coins', () => { // Find the exact transaction (sent one is the one with user[1] as user) const transaction = await Transaction.find({ userId: user[1].id, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( @@ -350,7 +349,7 @@ describe('send coins', () => { // Find the exact transaction (received one is the one with user[0] as user) const transaction = await Transaction.find({ userId: user[0].id, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 486ed87d2..afe8a7974 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -39,6 +39,7 @@ import { findUserByEmail } from './UserResolver' import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' +import { Decay } from '../model/Decay' export const executeTransaction = async ( amount: Decimal, @@ -68,17 +69,8 @@ export const executeTransaction = async ( // validate amount const receivedCallDate = new Date() - const sendBalance = await calculateBalance( - sender.id, - amount.mul(-1), - receivedCallDate, - transactionLink, - ) - logger.debug(`calculated Balance=${sendBalance}`) - if (!sendBalance) { - logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`) - throw new Error("user hasn't enough GDD or amount is < 0") - } + + const sendBalance = await calculateBalance(sender.id, amount, receivedCallDate, transactionLink) const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() @@ -91,7 +83,7 @@ export const executeTransaction = async ( transactionSend.memo = memo transactionSend.userId = sender.id transactionSend.linkedUserId = recipient.id - transactionSend.amount = amount.mul(-1) + transactionSend.amount = amount transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate transactionSend.decay = sendBalance.decay.decay @@ -108,7 +100,24 @@ export const executeTransaction = async ( transactionReceive.userId = recipient.id transactionReceive.linkedUserId = sender.id transactionReceive.amount = amount - const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) + + // state received balance + let receiveBalance: { + balance: Decimal + decay: Decay + lastTransactionId: number + } | null + + // try received balance + try { + receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) + } catch (e) { + logger.info( + `User with no transactions sent: ${recipient.id}, has received a transaction of ${amount} GDD from user: ${sender.id}`, + ) + receiveBalance = null + } + transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount transactionReceive.balanceDate = receivedCallDate transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 9abb31554..65214ebb5 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,5 +1,17 @@ +import Decimal from 'decimal.js-light' + export const objectValuesToArray = (obj: { [x: string]: string }): Array => { return Object.keys(obj).map(function (key) { return obj[key] }) } + +// to improve code readability, as String is needed, it is handled inside this utility function +export const decimalAddition = (a: Decimal, b: Decimal): Decimal => { + return a.add(b.toString()) +} + +// to improve code readability, as String is needed, it is handled inside this utility function +export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => { + return a.minus(b.toString()) +} diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index df1d4b1c0..64422b64f 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -5,6 +5,8 @@ import { Decay } from '@model/Decay' import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { decimalSubtraction, decimalAddition } from './utilities' +import { logger } from '@test/testSetup' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -23,16 +25,26 @@ async function calculateBalance( amount: Decimal, time: Date, transactionLink?: dbTransactionLink | null, -): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { +): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number }> { + // negative or empty amount should not be allowed + if (amount.lessThanOrEqualTo(0)) { + logger.error(`Transaction amount must be greater than 0: ${amount}`) + throw new Error('Transaction amount must be greater than 0') + } + + // check if user has prior transactions const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } }) - if (!lastTransaction) return null - // negative amount should not be allowed - if (amount.greaterThan(0)) return null + + if (!lastTransaction) { + logger.error(`No prior transaction found for user with id: ${userId}`) + throw new Error('User has not received any GDD yet') + } const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) - // TODO why we have to use toString() here? - const balance = decay.balance.add(amount.toString()) + // new balance is the old balance minus the amount used + const balance = decimalSubtraction(decay.balance, amount) + const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time) @@ -40,11 +52,16 @@ async function calculateBalance( // else we cannot redeem links which are more or equal to half of what an account actually owns const releasedLinkAmount = transactionLink ? transactionLink.holdAvailableAmount : new Decimal(0) - if ( - balance.minus(sumHoldAvailableAmount.toString()).plus(releasedLinkAmount.toString()).lessThan(0) - ) { - return null + const availableBalance = decimalSubtraction(balance, sumHoldAvailableAmount) + + if (decimalAddition(availableBalance, releasedLinkAmount).lessThan(0)) { + logger.error( + `Not enough funds for a transaction of ${amount} GDD, user with id: ${userId} has only ${balance} GDD available`, + ) + throw new Error('Not enough funds for transaction') } + + logger.debug(`calculated Balance=${balance}`) return { balance, lastTransactionId: lastTransaction.id, decay } }