mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
314 lines
11 KiB
TypeScript
314 lines
11 KiB
TypeScript
import { backendLogger as logger } from '@/server/logger'
|
|
import { Context, getUser } from '@/server/context'
|
|
import { getConnection } from '@dbTools/typeorm'
|
|
import {
|
|
Resolver,
|
|
Args,
|
|
Arg,
|
|
Authorized,
|
|
Ctx,
|
|
Mutation,
|
|
Query,
|
|
Int,
|
|
createUnionType,
|
|
} from 'type-graphql'
|
|
import { TransactionLink } from '@model/TransactionLink'
|
|
import { ContributionLink } from '@model/ContributionLink'
|
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
|
import { User as dbUser } from '@entity/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'
|
|
import { Contribution as DbContribution } from '@entity/Contribution'
|
|
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
|
import { isContributionValid } from './util/isContributionValid'
|
|
import { getUserCreation } from './util/getUserCreation'
|
|
import { Decay } from '@model/Decay'
|
|
import Decimal from 'decimal.js-light'
|
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
|
|
|
const QueryLinkResult = createUnionType({
|
|
name: 'QueryLinkResult', // the name of the GraphQL union
|
|
types: () => [TransactionLink, ContributionLink] as const, // function that returns tuple of object types classes
|
|
})
|
|
|
|
// TODO: do not export, test it inside the resolver
|
|
export const transactionLinkCode = (date: Date): string => {
|
|
const time = date.getTime().toString(16)
|
|
return (
|
|
randomBytes(12)
|
|
.toString('hex')
|
|
.substring(0, 24 - time.length) + time
|
|
)
|
|
}
|
|
|
|
const CODE_VALID_DAYS_DURATION = 14
|
|
|
|
export const transactionLinkExpireDate = (date: Date): Date => {
|
|
const validUntil = new Date(date)
|
|
return new Date(validUntil.setDate(date.getDate() + CODE_VALID_DAYS_DURATION))
|
|
}
|
|
|
|
@Resolver()
|
|
export class TransactionLinkResolver {
|
|
@Authorized([RIGHTS.CREATE_TRANSACTION_LINK])
|
|
@Mutation(() => TransactionLink)
|
|
async createTransactionLink(
|
|
@Args() { amount, memo }: TransactionLinkArgs,
|
|
@Ctx() context: Context,
|
|
): Promise<TransactionLink> {
|
|
const user = getUser(context)
|
|
|
|
const createdDate = new Date()
|
|
const validUntil = transactionLinkExpireDate(createdDate)
|
|
|
|
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")
|
|
}
|
|
|
|
const transactionLink = dbTransactionLink.create()
|
|
transactionLink.userId = user.id
|
|
transactionLink.amount = amount
|
|
transactionLink.memo = memo
|
|
transactionLink.holdAvailableAmount = holdAvailableAmount
|
|
transactionLink.code = transactionLinkCode(createdDate)
|
|
transactionLink.createdAt = createdDate
|
|
transactionLink.validUntil = validUntil
|
|
await dbTransactionLink.save(transactionLink).catch(() => {
|
|
throw new Error('Unable to save transaction link')
|
|
})
|
|
|
|
return new TransactionLink(transactionLink, new User(user))
|
|
}
|
|
|
|
@Authorized([RIGHTS.DELETE_TRANSACTION_LINK])
|
|
@Mutation(() => Boolean)
|
|
async deleteTransactionLink(
|
|
@Arg('id', () => Int) id: number,
|
|
@Ctx() context: Context,
|
|
): Promise<boolean> {
|
|
const user = getUser(context)
|
|
|
|
const transactionLink = await dbTransactionLink.findOne({ id })
|
|
if (!transactionLink) {
|
|
throw new Error('Transaction Link not found!')
|
|
}
|
|
|
|
if (transactionLink.userId !== user.id) {
|
|
throw new Error('Transaction Link cannot be deleted!')
|
|
}
|
|
|
|
if (transactionLink.redeemedBy) {
|
|
throw new Error('Transaction Link already redeemed!')
|
|
}
|
|
|
|
await transactionLink.softRemove().catch(() => {
|
|
throw new Error('Transaction Link could not be deleted!')
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
|
@Query(() => QueryLinkResult)
|
|
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
|
if (code.match(/^CL-/)) {
|
|
const contributionLink = await DbContributionLink.findOneOrFail(
|
|
{ code: code.replace('CL-', '') },
|
|
{ withDeleted: true },
|
|
)
|
|
return new ContributionLink(contributionLink)
|
|
} else {
|
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
|
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), redeemedBy)
|
|
}
|
|
}
|
|
|
|
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
|
@Query(() => [TransactionLink])
|
|
async listTransactionLinks(
|
|
@Args()
|
|
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
|
@Ctx() context: Context,
|
|
): Promise<TransactionLink[]> {
|
|
const user = getUser(context)
|
|
// 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('code', () => String) code: string,
|
|
@Ctx() context: Context,
|
|
): Promise<boolean> {
|
|
const user = getUser(context)
|
|
const now = new Date()
|
|
|
|
if (code.match(/^CL-/)) {
|
|
logger.info('redeem contribution link...')
|
|
const queryRunner = getConnection().createQueryRunner()
|
|
await queryRunner.connect()
|
|
await queryRunner.startTransaction('SERIALIZABLE')
|
|
try {
|
|
const contributionLink = await queryRunner.manager
|
|
.createQueryBuilder()
|
|
.select('contributionLink')
|
|
.from(DbContributionLink, 'contributionLink')
|
|
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
|
.getOne()
|
|
if (!contributionLink) {
|
|
logger.error('no contribution link found to given code:', code)
|
|
throw new Error('No contribution link found')
|
|
}
|
|
logger.info('...contribution link found with id', contributionLink.id)
|
|
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
|
logger.error(
|
|
'contribution link is not valid yet. Valid from: ',
|
|
contributionLink.validFrom,
|
|
)
|
|
throw new Error('Contribution link not valid yet')
|
|
}
|
|
if (contributionLink.validTo) {
|
|
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
|
|
logger.error('contribution link is depricated. Valid to: ', contributionLink.validTo)
|
|
throw new Error('Contribution link is depricated')
|
|
}
|
|
}
|
|
if (contributionLink.cycle !== 'ONCE') {
|
|
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
|
throw new Error('Contribution link has unknown cycle')
|
|
}
|
|
// Test ONCE rule
|
|
const alreadyRedeemed = await queryRunner.manager
|
|
.createQueryBuilder()
|
|
.select('contribution')
|
|
.from(DbContribution, 'contribution')
|
|
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
|
linkId: contributionLink.id,
|
|
id: user.id,
|
|
})
|
|
.getOne()
|
|
if (alreadyRedeemed) {
|
|
logger.error('contribution link with rule ONCE already redeemed by user with id', user.id)
|
|
throw new Error('Contribution link already redeemed')
|
|
}
|
|
|
|
const creations = await getUserCreation(user.id, false)
|
|
logger.info('open creations', creations)
|
|
if (!isContributionValid(creations, contributionLink.amount, now)) {
|
|
logger.error(
|
|
'Amount of Contribution link exceeds available amount for this month',
|
|
contributionLink.amount,
|
|
)
|
|
throw new Error('Amount of Contribution link exceeds available amount')
|
|
}
|
|
const contribution = new DbContribution()
|
|
contribution.userId = user.id
|
|
contribution.createdAt = now
|
|
contribution.contributionDate = now
|
|
contribution.memo = contributionLink.memo
|
|
contribution.amount = contributionLink.amount
|
|
contribution.contributionLinkId = contributionLink.id
|
|
await queryRunner.manager.insert(DbContribution, contribution)
|
|
|
|
const lastTransaction = await queryRunner.manager
|
|
.createQueryBuilder()
|
|
.select('transaction')
|
|
.from(DbTransaction, 'transaction')
|
|
.where('transaction.userId = :id', { id: user.id })
|
|
.orderBy('transaction.balanceDate', 'DESC')
|
|
.getOne()
|
|
let newBalance = new Decimal(0)
|
|
|
|
let decay: Decay | null = null
|
|
if (lastTransaction) {
|
|
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
|
newBalance = decay.balance
|
|
}
|
|
newBalance = newBalance.add(contributionLink.amount.toString())
|
|
|
|
const transaction = new DbTransaction()
|
|
transaction.typeId = TransactionTypeId.CREATION
|
|
transaction.memo = contribution.memo
|
|
transaction.userId = contribution.userId
|
|
transaction.previous = lastTransaction ? lastTransaction.id : null
|
|
transaction.amount = contribution.amount
|
|
transaction.creationDate = contribution.contributionDate
|
|
transaction.balance = newBalance
|
|
transaction.balanceDate = now
|
|
transaction.decay = decay ? decay.decay : new Decimal(0)
|
|
transaction.decayStart = decay ? decay.start : null
|
|
await queryRunner.manager.insert(DbTransaction, transaction)
|
|
|
|
contribution.confirmedAt = now
|
|
contribution.transactionId = transaction.id
|
|
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
|
|
|
await queryRunner.commitTransaction()
|
|
logger.info('creation from contribution link commited successfuly.')
|
|
} catch (e) {
|
|
await queryRunner.rollbackTransaction()
|
|
logger.error(`Creation from contribution link was not successful: ${e}`)
|
|
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
|
} finally {
|
|
await queryRunner.release()
|
|
}
|
|
return true
|
|
} else {
|
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
|
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
|
|
|
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,
|
|
transactionLink,
|
|
)
|
|
|
|
return true
|
|
}
|
|
}
|
|
}
|