diff --git a/backend/src/auth/INALIENABLE_RIGHTS.ts b/backend/src/auth/INALIENABLE_RIGHTS.ts index 10d745806..fa9ea2224 100644 --- a/backend/src/auth/INALIENABLE_RIGHTS.ts +++ b/backend/src/auth/INALIENABLE_RIGHTS.ts @@ -7,4 +7,5 @@ export const INALIENABLE_RIGHTS = [ RIGHTS.CREATE_USER, RIGHTS.SEND_RESET_PASSWORD_EMAIL, RIGHTS.SET_PASSWORD, + RIGHTS.QUERY_TRANSACTION_LINK, ] diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 6bcd3fa43..df4d4a2c1 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -20,6 +20,7 @@ export enum RIGHTS { HAS_ELOPAGE = 'HAS_ELOPAGE', CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK', QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK', + // Admin SEARCH_USERS = 'SEARCH_USERS', CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION', diff --git a/backend/src/graphql/arg/QueryTransactionLinkArgs.ts b/backend/src/graphql/arg/QueryTransactionLinkArgs.ts new file mode 100644 index 000000000..2dcd29572 --- /dev/null +++ b/backend/src/graphql/arg/QueryTransactionLinkArgs.ts @@ -0,0 +1,10 @@ +import { ArgsType, Field, Int } from 'type-graphql' + +@ArgsType() +export default class QueryTransactionLinkArgs { + @Field(() => String) + code: string + + @Field(() => Int, { nullable: true }) + redeemUserId?: number +} diff --git a/backend/src/graphql/model/TransactionLink.ts b/backend/src/graphql/model/TransactionLink.ts index 52fc6ce78..f41d34acc 100644 --- a/backend/src/graphql/model/TransactionLink.ts +++ b/backend/src/graphql/model/TransactionLink.ts @@ -5,7 +5,7 @@ import { User } from './User' @ObjectType() export class TransactionLink { - constructor(transactionLink: dbTransactionLink, user: User) { + constructor(transactionLink: dbTransactionLink, user: User, redeemedBy: User | null = null) { this.id = transactionLink.id this.user = user this.amount = transactionLink.amount @@ -15,8 +15,9 @@ export class TransactionLink { this.createdAt = transactionLink.createdAt this.validUntil = transactionLink.validUntil this.showEmail = transactionLink.showEmail - this.redeemedAt = null - this.redeemedBy = null + this.deletedAt = transactionLink.deletedAt + this.redeemedAt = transactionLink.redeemedAt + this.redeemedBy = redeemedBy } @Field(() => Number) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 61926ba28..4065bb13a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Resolver, Args, Authorized, Ctx, Mutation, Query, Arg } from 'type-graphql' -import { getCustomRepository, MoreThan } from '@dbTools/typeorm' +import { Resolver, Args, Authorized, Ctx, Mutation, Query } from 'type-graphql' +import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLink } from '@model/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import TransactionLinkArgs from '@arg/TransactionLinkArgs' +import QueryTransactionLinkArgs from '@arg/QueryTransactionLinkArgs' import { UserRepository } from '@repository/User' import { calculateBalance } from '@/util/validate' import { RIGHTS } from '@/auth/RIGHTS' @@ -23,10 +24,11 @@ export const transactionLinkCode = (date: Date): string => { ) } +const CODE_VALID_DAYS_DURATION = 14 + const transactionLinkExpireDate = (date: Date): Date => { const validUntil = new Date(date) - // valid for 14 days - return new Date(validUntil.setDate(date.getDate() + 14)) + return new Date(validUntil.setDate(date.getDate() + CODE_VALID_DAYS_DURATION)) } @Resolver() @@ -43,24 +45,10 @@ export class TransactionLinkResolver { const createdDate = new Date() const validUntil = transactionLinkExpireDate(createdDate) - const holdAvailableAmount = amount.add( - calculateDecay(amount, createdDate, validUntil).decay.mul(-1), - ) - - const openTransactionLinks = await dbTransactionLink.find({ - select: ['holdAvailableAmount'], - where: { userId: user.id, redeemedAt: null, validUntil: MoreThan(createdDate) }, - }) - - const holdAvailable = openTransactionLinks.reduce( - (previousValue, currentValue) => - previousValue.add(currentValue.holdAvailableAmount.toString()), - holdAvailableAmount, - ) + const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) // validate amount - // TODO taken from transaction resolver, duplicate code - const sendBalance = await calculateBalance(user.id, holdAvailable.mul(-1), createdDate) + const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) if (!sendBalance) { throw new Error("user hasn't enough GDD or amount is < 0") } @@ -74,8 +62,8 @@ export class TransactionLinkResolver { transactionLink.createdAt = createdDate transactionLink.validUntil = validUntil transactionLink.showEmail = showEmail - await dbTransactionLink.save(transactionLink).catch((error) => { - throw error + await dbTransactionLink.save(transactionLink).catch(() => { + throw new Error('Unable to save transaction link') }) return new TransactionLink(transactionLink, new User(user)) @@ -83,11 +71,30 @@ export class TransactionLinkResolver { @Authorized([RIGHTS.QUERY_TRANSACTION_LINK]) @Query(() => TransactionLink) - async queryTransactionLink(@Arg('code') code: string): Promise { - console.log(code) - const transactionLink = await dbTransactionLink.findOneOrFail({ code: code }) + async queryTransactionLink( + @Args() { code, redeemUserId }: QueryTransactionLinkArgs, + ): Promise { + const transactionLink = await dbTransactionLink.findOneOrFail({ code }) const userRepository = getCustomRepository(UserRepository) const user = await userRepository.findOneOrFail({ id: transactionLink.userId }) - return new TransactionLink(transactionLink, new User(user)) + let userRedeem = null + if (redeemUserId && !transactionLink.redeemedBy) { + const redeemedByUser = await userRepository.findOne({ id: redeemUserId }) + if (!redeemedByUser) { + throw new Error('Unable to find user that redeem the link') + } + userRedeem = new User(redeemedByUser) + transactionLink.redeemedBy = userRedeem.id + await dbTransactionLink.save(transactionLink).catch(() => { + throw new Error('Unable to save transaction link') + }) + } else if (transactionLink.redeemedBy) { + const redeemedByUser = await userRepository.findOne({ id: redeemUserId }) + if (!redeemedByUser) { + throw new Error('Unable to find user that has redeemed the link') + } + userRedeem = new User(redeemedByUser) + } + return new TransactionLink(transactionLink, new User(user), userRedeem) } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index d674fc69e..1d1a00c39 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -19,6 +19,7 @@ import { Order } from '@enum/Order' import { UserRepository } from '@repository/User' import { TransactionRepository } from '@repository/Transaction' +import { TransactionLinkRepository } from '@repository/TransactionLink' import { User as dbUser } from '@entity/User' import { Transaction as dbTransaction } from '@entity/Transaction' @@ -127,9 +128,14 @@ export class TransactionResolver { transactions.push(new Transaction(userTransaction, self, linkedUser)) }) + const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) + const toHoldAvailable = await transactionLinkRepository.sumAmountToHoldAvailable(user.id, now) + // Construct Result return new TransactionList( - calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance, + calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance.minus( + toHoldAvailable.toString(), + ), transactions, userTransactionsCount, balanceGDT, diff --git a/backend/src/typeorm/repository/TransactionLink.ts b/backend/src/typeorm/repository/TransactionLink.ts new file mode 100644 index 000000000..5f7531bc8 --- /dev/null +++ b/backend/src/typeorm/repository/TransactionLink.ts @@ -0,0 +1,16 @@ +import { Repository, EntityRepository } from '@dbTools/typeorm' +import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import Decimal from 'decimal.js-light' + +@EntityRepository(dbTransactionLink) +export class TransactionLinkRepository extends Repository { + async sumAmountToHoldAvailable(userId: number, date: Date): Promise { + const { sum } = await this.createQueryBuilder('transactionLinks') + .select('SUM(transactionLinks.holdAvailableAmount)', 'sum') + .where('transactionLinks.userId = :userId', { userId }) + .andWhere('transactionLinks.redeemedAt is NULL') + .andWhere('transactionLinks.validUntil > :date', { date }) + .getRawOne() + return sum ? new Decimal(sum) : new Decimal(0) + } +} diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 3699fe511..0858ea50f 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -2,6 +2,8 @@ import { calculateDecay } from './decay' import Decimal from 'decimal.js-light' import { Transaction } from '@entity/Transaction' import { Decay } from '@model/Decay' +import { getCustomRepository } from '@dbTools/typeorm' +import { TransactionLinkRepository } from '@repository/TransactionLink' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -24,9 +26,13 @@ async function calculateBalance( if (!lastTransaction) return null const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + // TODO why we have to use toString() here? const balance = decay.balance.add(amount.toString()) - if (balance.lessThan(0)) { + const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) + const toHoldAvailable = await transactionLinkRepository.sumAmountToHoldAvailable(userId, time) + + if (balance.minus(toHoldAvailable.toString()).lessThan(0)) { return null } return { balance, lastTransactionId: lastTransaction.id, decay } diff --git a/database/entity/0030-transaction_link/TransactionLink.ts b/database/entity/0030-transaction_link/TransactionLink.ts index 6ea708547..177f23561 100644 --- a/database/entity/0030-transaction_link/TransactionLink.ts +++ b/database/entity/0030-transaction_link/TransactionLink.ts @@ -42,7 +42,7 @@ export class TransactionLink extends BaseEntity { createdAt: Date @DeleteDateColumn() - deletedAt?: Date | null + deletedAt: Date | null @Column({ type: 'datetime', @@ -61,8 +61,8 @@ export class TransactionLink extends BaseEntity { type: 'datetime', nullable: true, }) - redeemedAt?: Date | null + redeemedAt: Date | null @Column({ type: 'int', unsigned: true, nullable: true }) - redeemedBy?: number | null + redeemedBy: number | null }