Merge branch 'master' into 1599-components-for-transactionlist-types

This commit is contained in:
Alexander Friedland 2022-03-11 16:37:12 +01:00 committed by GitHub
commit 5238a0a5d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 286 additions and 165 deletions

View File

@ -21,6 +21,8 @@ export enum RIGHTS {
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',

View File

@ -20,6 +20,8 @@ export const ROLE_USER = new Role('user', [
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -1,10 +0,0 @@
import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType()
export default class QueryTransactionLinkArgs {
@Field(() => String)
code: string
@Field(() => Int, { nullable: true })
redeemUserId?: number
}

View File

@ -6,6 +6,7 @@ export enum TransactionTypeId {
RECEIVE = 3,
// This is a virtual property, never occurring on the database
DECAY = 4,
TRANSACTION_LINK = 5,
}
registerEnumType(TransactionTypeId, {

View File

@ -5,14 +5,17 @@ import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query } from 'type-grap
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 { User as dbUser } from '@entity/User'
import { UserRepository } from '@repository/User'
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
import Paginated from '@arg/Paginated'
import { calculateBalance } from '@/util/validate'
import { RIGHTS } from '@/auth/RIGHTS'
import { randomBytes } from 'crypto'
import { User } from '@model/User'
import { calculateDecay } from '@/util/decay'
import { executeTransaction } from './TransactionResolver'
import { Order } from '@enum/Order'
// TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => {
@ -96,30 +99,72 @@ export class TransactionLinkResolver {
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
@Query(() => TransactionLink)
async queryTransactionLink(
@Args() { code, redeemUserId }: QueryTransactionLinkArgs,
): Promise<TransactionLink> {
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findOneOrFail({ id: transactionLink.userId })
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)
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
let redeemedBy: User | null = null
if (transactionLink && transactionLink.redeemedBy) {
redeemedBy = new User(await dbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
}
return new TransactionLink(transactionLink, new User(user), userRedeem)
return new TransactionLink(transactionLink, new User(user), redeemedBy)
}
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
@Query(() => [TransactionLink])
async listTransactionLinks(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Ctx() context: any,
): Promise<TransactionLink[]> {
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByPubkeyHex(context.pubKey)
// const now = new Date()
const transactionLinks = await dbTransactionLink.find({
where: {
userId: user.id,
redeemedBy: null,
// validUntil: MoreThan(now),
},
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return transactionLinks.map((tl) => new TransactionLink(tl, new User(user)))
}
@Authorized([RIGHTS.REDEEM_TRANSACTION_LINK])
@Mutation(() => Boolean)
async redeemTransactionLink(@Arg('id') id: number, @Ctx() context: any): Promise<boolean> {
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByPubkeyHex(context.pubKey)
const transactionLink = await dbTransactionLink.findOneOrFail({ id })
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
const now = new Date()
if (user.id === linkedUser.id) {
throw new Error('Cannot redeem own transaction link.')
}
if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new Error('Transaction Link is not valid anymore.')
}
if (transactionLink.redeemedBy) {
throw new Error('Transaction Link already redeemed.')
}
await executeTransaction(transactionLink.amount, transactionLink.memo, linkedUser, user)
// TODO: Rollback transaction when updating links fails
transactionLink.redeemedAt = now
transactionLink.redeemedBy = user.id
transactionLink.save().catch(() => {
throw new Error('Could not update transaction link.')
})
return true
}
}

View File

@ -30,10 +30,87 @@ import { calculateBalance, isHexPublicKey } from '@/util/validate'
import { RIGHTS } from '@/auth/RIGHTS'
import { User } from '@model/User'
import { communityUser } from '@/util/communityUser'
import { virtualDecayTransaction } from '@/util/virtualDecayTransaction'
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
import Decimal from 'decimal.js-light'
import { calculateDecay } from '@/util/decay'
export const executeTransaction = async (
amount: Decimal,
memo: string,
sender: dbUser,
recipient: dbUser,
): Promise<boolean> => {
if (sender.id === recipient.id) {
throw new Error('Sender and Recipient are the same.')
}
// validate amount
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(sender.id, amount.mul(-1), receivedCallDate)
if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0")
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = sender.id
transactionSend.linkedUserId = recipient.id
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
await queryRunner.manager.insert(dbTransaction, transactionSend)
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipient.id
transactionReceive.linkedUserId = sender.id
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
await queryRunner.manager.insert(dbTransaction, transactionReceive)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
// send notification email
// TODO: translate
await sendTransactionReceivedEmail({
senderFirstName: sender.firstName,
senderLastName: sender.lastName,
recipientFirstName: recipient.firstName,
recipientLastName: recipient.lastName,
email: recipient.email,
amount,
memo,
})
return true
}
@Resolver()
export class TransactionResolver {
@Authorized([RIGHTS.TRANSACTION_LIST])
@ -112,11 +189,29 @@ export class TransactionResolver {
const self = new User(user)
const transactions: Transaction[] = []
// decay transaction
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate } =
await transactionLinkRepository.summary(user.id, now)
// decay & link transactions
if (!onlyCreations && currentPage === 1 && order === Order.DESC) {
transactions.push(
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
)
// virtual transaction for pending transaction-links sum
if (sumHoldAvailableAmount.greaterThan(0)) {
transactions.push(
virtualLinkTransaction(
lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
sumAmount,
sumHoldAvailableAmount,
sumHoldAvailableAmount.minus(sumAmount.toString()),
firstDate || now,
lastDate || now,
self,
),
)
}
}
// transactions
@ -128,13 +223,10 @@ 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.minus(
toHoldAvailable.toString(),
sumHoldAvailableAmount.toString(),
),
transactions,
userTransactionsCount,
@ -154,12 +246,6 @@ export class TransactionResolver {
if (senderUser.pubKey.length !== 32) {
throw new Error('invalid sender public key')
}
// validate amount
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(senderUser.id, amount.mul(-1), receivedCallDate)
if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0")
}
// validate recipient user
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
@ -173,62 +259,7 @@ export class TransactionResolver {
throw new Error('invalid recipient public key')
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = senderUser.id
transactionSend.linkedUserId = recipientUser.id
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
await queryRunner.manager.insert(dbTransaction, transactionSend)
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
transactionReceive.userId = recipientUser.id
transactionReceive.linkedUserId = senderUser.id
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
transactionReceive.balanceDate = receivedCallDate
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
transactionReceive.linkedTransactionId = transactionSend.id
await queryRunner.manager.insert(dbTransaction, transactionReceive)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
// send notification email
// TODO: translate
await sendTransactionReceivedEmail({
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
recipientFirstName: recipientUser.firstName,
recipientLastName: recipientUser.lastName,
email: recipientUser.email,
amount,
memo,
})
await executeTransaction(amount, memo, senderUser, recipientUser)
return true
}

View File

@ -4,13 +4,33 @@ import Decimal from 'decimal.js-light'
@EntityRepository(dbTransactionLink)
export class TransactionLinkRepository extends Repository<dbTransactionLink> {
async sumAmountToHoldAvailable(userId: number, date: Date): Promise<Decimal> {
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)
async summary(
userId: number,
date: Date,
): Promise<{
sumHoldAvailableAmount: Decimal
sumAmount: Decimal
lastDate: Date | null
firstDate: Date | null
}> {
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate } =
await this.createQueryBuilder('transactionLinks')
.select('SUM(transactionLinks.holdAvailableAmount)', 'sumHoldAvailableAmount')
.addSelect('SUM(transactionLinks.amount)', 'sumAmount')
.addSelect('MAX(transactionLinks.validUntil)', 'lastDate')
.addSelect('MIN(transactionLinks.createdAt)', 'firstDate')
.where('transactionLinks.userId = :userId', { userId })
.andWhere('transactionLinks.redeemedAt is NULL')
.andWhere('transactionLinks.validUntil > :date', { date })
.orderBy('transactionLinks.createdAt', 'DESC')
.getRawOne()
return {
sumHoldAvailableAmount: sumHoldAvailableAmount
? new Decimal(sumHoldAvailableAmount)
: new Decimal(0),
sumAmount: sumAmount ? new Decimal(sumAmount) : new Decimal(0),
lastDate: lastDate || null,
firstDate: firstDate || null,
}
}
}

View File

@ -30,9 +30,9 @@ async function calculateBalance(
// TODO why we have to use toString() here?
const balance = decay.balance.add(amount.toString())
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
const toHoldAvailable = await transactionLinkRepository.sumAmountToHoldAvailable(userId, time)
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
if (balance.minus(toHoldAvailable.toString()).lessThan(0)) {
if (balance.minus(sumHoldAvailableAmount.toString()).lessThan(0)) {
return null
}
return { balance, lastTransactionId: lastTransaction.id, decay }

View File

@ -1,52 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import Decimal from 'decimal.js-light'
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { calculateDecay } from './decay'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { Transaction } from '@model/Transaction'
import { User } from '@model/User'
const virtualDecayTransaction = (
balance: Decimal,
balanceDate: Date,
time: Date = new Date(),
user: User,
): Transaction => {
const decay = calculateDecay(balance, balanceDate, time)
// const balance = decay.balance.minus(lastTransaction.balance)
const decayDbTransaction: dbTransaction = {
id: -1,
userId: -1,
previous: -1,
typeId: TransactionTypeId.DECAY,
amount: decay.decay ? decay.decay : new Decimal(0), // new Decimal(0), // this kinda is wrong, but helps with the frontend query
balance: decay.balance,
balanceDate: time,
decay: decay.decay ? decay.decay : new Decimal(0),
decayStart: decay.start,
memo: '',
creationDate: null,
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
save: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
remove: function (options?: RemoveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
softRemove: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
recover: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
reload: function (): Promise<void> {
throw new Error('Function not implemented.')
},
}
return new Transaction(decayDbTransaction, user)
}
export { virtualDecayTransaction }

View File

@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Transaction } from '@model/Transaction'
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { calculateDecay } from './decay'
import { User } from '@model/User'
import Decimal from 'decimal.js-light'
const defaultModelFunctions = {
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
save: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
remove: function (options?: RemoveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
softRemove: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
recover: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
reload: function (): Promise<void> {
throw new Error('Function not implemented.')
},
}
const virtualLinkTransaction = (
balance: Decimal,
amount: Decimal,
holdAvailableAmount: Decimal,
decay: Decimal,
createdAt: Date,
validUntil: Date,
user: User,
): Transaction => {
const linkDbTransaction: dbTransaction = {
id: -2,
userId: -1,
previous: -1,
typeId: TransactionTypeId.TRANSACTION_LINK,
amount: amount,
balance: balance,
balanceDate: validUntil,
decayStart: createdAt,
decay: decay,
memo: '',
creationDate: null,
...defaultModelFunctions,
}
return new Transaction(linkDbTransaction, user)
}
const virtualDecayTransaction = (
balance: Decimal,
balanceDate: Date,
time: Date = new Date(),
user: User,
): Transaction => {
const decay = calculateDecay(balance, balanceDate, time)
// const balance = decay.balance.minus(lastTransaction.balance)
const decayDbTransaction: dbTransaction = {
id: -1,
userId: -1,
previous: -1,
typeId: TransactionTypeId.DECAY,
amount: decay.decay ? decay.decay : new Decimal(0), // new Decimal(0), // this kinda is wrong, but helps with the frontend query
balance: decay.balance,
balanceDate: time,
decay: decay.decay ? decay.decay : new Decimal(0),
decayStart: decay.start,
memo: '',
creationDate: null,
...defaultModelFunctions,
}
return new Transaction(decayDbTransaction, user)
}
export { virtualLinkTransaction, virtualDecayTransaction }