mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
backend compiles
This commit is contained in:
parent
f3f2d547a3
commit
7644bf1834
@ -24,6 +24,7 @@
|
|||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graphql": "^15.5.1",
|
"graphql": "^15.5.1",
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||||
|
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
// Set precision value
|
||||||
|
// TODO test if this works here
|
||||||
|
Decimal.set({
|
||||||
|
precision: 25,
|
||||||
|
rounding: Decimal.ROUND_HALF_UP,
|
||||||
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0027-decimal_types',
|
DB_VERSION: '0027-decimal_types',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { ArgsType, Field } from 'type-graphql'
|
import { ArgsType, Field } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export default class TransactionSendArgs {
|
export default class TransactionSendArgs {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Decimal)
|
||||||
amount: number
|
amount: Decimal
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
memo: string
|
memo: string
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Balance {
|
export class Balance {
|
||||||
constructor(json: any) {
|
constructor(json: any) {
|
||||||
this.balance = Number(json.balance)
|
this.balance = json.balance
|
||||||
this.decay = Number(json.decay)
|
this.decay = json.decay
|
||||||
this.decayDate = json.decay_date
|
this.decayDate = json.decay_date
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Decimal)
|
||||||
balance: number
|
balance: Decimal
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Decimal)
|
||||||
decay: number
|
decay: Decimal
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
decayDate: string
|
decayDate: string
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
import { Decay } from './Decay'
|
import { Decay } from './Decay'
|
||||||
|
|
||||||
@ -12,19 +13,19 @@ import { Decay } from './Decay'
|
|||||||
export class Transaction {
|
export class Transaction {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = ''
|
this.type = ''
|
||||||
this.balance = 0
|
this.balance = new Decimal(0)
|
||||||
this.totalBalance = 0
|
this.totalBalance = new Decimal(0)
|
||||||
this.memo = ''
|
this.memo = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Decimal)
|
||||||
balance: number
|
balance: Decimal
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Decimal)
|
||||||
totalBalance: number
|
totalBalance: Decimal
|
||||||
|
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
decayStart?: string
|
decayStart?: string
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
import { Transaction } from './Transaction'
|
import { Transaction } from './Transaction'
|
||||||
|
|
||||||
@ -8,9 +9,8 @@ export class TransactionList {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.gdtSum = 0
|
this.gdtSum = 0
|
||||||
this.count = 0
|
this.count = 0
|
||||||
this.balance = 0
|
this.balance = new Decimal(0)
|
||||||
this.decay = 0
|
this.decayStartBlock = null
|
||||||
this.decayDate = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Number, { nullable: true })
|
@Field(() => Number, { nullable: true })
|
||||||
@ -20,13 +20,10 @@ export class TransactionList {
|
|||||||
count: number
|
count: number
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
balance: number
|
balance: Decimal
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Date, { nullable: true })
|
||||||
decay: number
|
decayStartBlock: Date | null
|
||||||
|
|
||||||
@Field(() => String)
|
|
||||||
decayDate: string
|
|
||||||
|
|
||||||
@Field(() => [Transaction])
|
@Field(() => [Transaction])
|
||||||
transactions: Transaction[]
|
transactions: Transaction[]
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { hasElopageBuys } from '../../util/hasElopageBuys'
|
|||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||||
import { Balance } from '@entity/Balance'
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
// const EMAIL_OPT_IN_REGISTER = 1
|
// const EMAIL_OPT_IN_REGISTER = 1
|
||||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||||
@ -306,35 +306,28 @@ export class AdminResolver {
|
|||||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||||
const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
||||||
|
|
||||||
let newBalance = 0
|
let newBalance = new Decimal(0)
|
||||||
if (lastUserTransaction) {
|
if (lastUserTransaction) {
|
||||||
newBalance = calculateDecay(
|
newBalance = calculateDecay(
|
||||||
Number(lastUserTransaction.balance),
|
lastUserTransaction.balance,
|
||||||
lastUserTransaction.balanceDate,
|
lastUserTransaction.balanceDate,
|
||||||
receivedCallDate,
|
receivedCallDate,
|
||||||
).balance
|
).balance
|
||||||
}
|
}
|
||||||
newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString()))
|
// TODO pending creations decimal
|
||||||
|
newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)))
|
||||||
|
|
||||||
const transaction = new Transaction()
|
const transaction = new Transaction()
|
||||||
transaction.typeId = TransactionTypeId.CREATION
|
transaction.typeId = TransactionTypeId.CREATION
|
||||||
transaction.memo = pendingCreation.memo
|
transaction.memo = pendingCreation.memo
|
||||||
transaction.userId = pendingCreation.userId
|
transaction.userId = pendingCreation.userId
|
||||||
transaction.amount = BigInt(parseInt(pendingCreation.amount.toString()))
|
// TODO pending creations decimal
|
||||||
|
transaction.amount = new Decimal(Number(pendingCreation.amount))
|
||||||
transaction.creationDate = pendingCreation.date
|
transaction.creationDate = pendingCreation.date
|
||||||
transaction.balance = BigInt(newBalance)
|
transaction.balance = newBalance
|
||||||
transaction.balanceDate = receivedCallDate
|
transaction.balanceDate = receivedCallDate
|
||||||
await transaction.save()
|
await transaction.save()
|
||||||
|
|
||||||
let userBalance = await Balance.findOne({ userId: pendingCreation.userId })
|
|
||||||
if (!userBalance) {
|
|
||||||
userBalance = new Balance()
|
|
||||||
userBalance.userId = pendingCreation.userId
|
|
||||||
}
|
|
||||||
userBalance.amount = Number(newBalance)
|
|
||||||
userBalance.modified = receivedCallDate
|
|
||||||
userBalance.recordDate = receivedCallDate
|
|
||||||
await userBalance.save()
|
|
||||||
await AdminPendingCreation.delete(pendingCreation)
|
await AdminPendingCreation.delete(pendingCreation)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import { getCustomRepository } from '@dbTools/typeorm'
|
|||||||
import { Balance } from '../model/Balance'
|
import { Balance } from '../model/Balance'
|
||||||
import { UserRepository } from '../../typeorm/repository/User'
|
import { UserRepository } from '../../typeorm/repository/User'
|
||||||
import { calculateDecay } from '../../util/decay'
|
import { calculateDecay } from '../../util/decay'
|
||||||
import { roundFloorFrom4 } from '../../util/round'
|
|
||||||
import { RIGHTS } from '../../auth/RIGHTS'
|
import { RIGHTS } from '../../auth/RIGHTS'
|
||||||
import { Balance as dbBalance } from '@entity/Balance'
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class BalanceResolver {
|
export class BalanceResolver {
|
||||||
@ -18,24 +18,26 @@ export class BalanceResolver {
|
|||||||
// load user and balance
|
// load user and balance
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
|
|
||||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||||
const balanceEntity = await dbBalance.findOne({ userId: userEntity.id })
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
|
const lastTransaction = await Transaction.findOne(
|
||||||
|
{ userId: user.id },
|
||||||
|
{ order: { balanceDate: 'DESC' } },
|
||||||
|
)
|
||||||
|
|
||||||
// No balance found
|
// No balance found
|
||||||
if (!balanceEntity) {
|
if (!lastTransaction) {
|
||||||
return new Balance({
|
return new Balance({
|
||||||
balance: 0,
|
balance: new Decimal(0),
|
||||||
decay: 0,
|
decay: new Decimal(0),
|
||||||
decay_date: now.toString(),
|
decay_date: now.toString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Balance({
|
return new Balance({
|
||||||
balance: roundFloorFrom4(balanceEntity.amount),
|
balance: lastTransaction.balance,
|
||||||
decay: roundFloorFrom4(
|
decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
||||||
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
|
|
||||||
),
|
|
||||||
decay_date: now.toString(),
|
decay_date: now.toString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||||
import { getCustomRepository, getConnection, QueryRunner } from '@dbTools/typeorm'
|
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||||
|
|
||||||
import CONFIG from '../../config'
|
import CONFIG from '../../config'
|
||||||
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
|
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
|
||||||
@ -20,68 +20,16 @@ import { Order } from '../enum/Order'
|
|||||||
import { UserRepository } from '../../typeorm/repository/User'
|
import { UserRepository } from '../../typeorm/repository/User'
|
||||||
import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
||||||
|
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser, User } from '@entity/User'
|
||||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||||
import { Balance as dbBalance } from '@entity/Balance'
|
|
||||||
|
|
||||||
import { apiPost } from '../../apis/HttpRequest'
|
import { apiPost } from '../../apis/HttpRequest'
|
||||||
import { roundFloorFrom4, roundCeilFrom4 } from '../../util/round'
|
|
||||||
import { calculateDecay } from '../../util/decay'
|
import { calculateDecay } from '../../util/decay'
|
||||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||||
import { TransactionType } from '../enum/TransactionType'
|
import { TransactionType } from '../enum/TransactionType'
|
||||||
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
import { calculateBalance, isHexPublicKey } from '../../util/validate'
|
||||||
import { RIGHTS } from '../../auth/RIGHTS'
|
import { RIGHTS } from '../../auth/RIGHTS'
|
||||||
|
|
||||||
// helper helper function
|
|
||||||
async function updateStateBalance(
|
|
||||||
user: dbUser,
|
|
||||||
balance: number,
|
|
||||||
received: Date,
|
|
||||||
queryRunner: QueryRunner,
|
|
||||||
): Promise<dbBalance> {
|
|
||||||
let userBalance = await dbBalance.findOne({ userId: user.id })
|
|
||||||
if (!userBalance) {
|
|
||||||
userBalance = new dbBalance()
|
|
||||||
userBalance.userId = user.id
|
|
||||||
userBalance.amount = balance
|
|
||||||
userBalance.modified = received
|
|
||||||
} else {
|
|
||||||
userBalance.amount = balance
|
|
||||||
userBalance.modified = new Date()
|
|
||||||
}
|
|
||||||
if (userBalance.amount <= 0) {
|
|
||||||
throw new Error('error new balance <= 0')
|
|
||||||
}
|
|
||||||
userBalance.recordDate = received
|
|
||||||
return queryRunner.manager.save(userBalance).catch((error) => {
|
|
||||||
throw new Error('error saving balance:' + error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function calculateNewBalance(
|
|
||||||
userId: number,
|
|
||||||
transactionDate: Date,
|
|
||||||
centAmount: number,
|
|
||||||
): Promise<number> {
|
|
||||||
let newBalance = centAmount
|
|
||||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
|
||||||
const lastUserTransaction = await transactionRepository.findLastForUser(userId)
|
|
||||||
if (lastUserTransaction) {
|
|
||||||
newBalance += Number(
|
|
||||||
calculateDecay(
|
|
||||||
Number(lastUserTransaction.balance),
|
|
||||||
lastUserTransaction.balanceDate,
|
|
||||||
transactionDate,
|
|
||||||
).balance,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newBalance <= 0) {
|
|
||||||
throw new Error('error new balance <= 0')
|
|
||||||
}
|
|
||||||
|
|
||||||
return newBalance
|
|
||||||
}
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class TransactionResolver {
|
export class TransactionResolver {
|
||||||
@Authorized([RIGHTS.TRANSACTION_LIST])
|
@Authorized([RIGHTS.TRANSACTION_LIST])
|
||||||
@ -97,21 +45,28 @@ export class TransactionResolver {
|
|||||||
}: Paginated,
|
}: Paginated,
|
||||||
@Ctx() context: any,
|
@Ctx() context: any,
|
||||||
): Promise<TransactionList> {
|
): Promise<TransactionList> {
|
||||||
// load user
|
// find user
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
|
// TODO: separate those usecases - this is a security issue
|
||||||
const user = userId
|
const user = userId
|
||||||
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
|
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
|
||||||
: await userRepository.findByPubkeyHex(context.pubKey)
|
: await userRepository.findByPubkeyHex(context.pubKey)
|
||||||
let limit = pageSize
|
|
||||||
let offset = 0
|
// find current balance
|
||||||
let skipFirstTransaction = false
|
const lastTransaction = await dbTransaction.findOne(
|
||||||
if (currentPage > 1) {
|
{ userId: user.id },
|
||||||
offset = (currentPage - 1) * pageSize - 1
|
{ order: { balanceDate: 'DESC' } },
|
||||||
limit++
|
)
|
||||||
}
|
|
||||||
if (offset && order === Order.ASC) {
|
if (!lastTransaction) {
|
||||||
offset--
|
// 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 transactionRepository = getCustomRepository(TransactionRepository)
|
||||||
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
|
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
|
||||||
user.id,
|
user.id,
|
||||||
@ -120,66 +75,52 @@ export class TransactionResolver {
|
|||||||
order,
|
order,
|
||||||
onlyCreations,
|
onlyCreations,
|
||||||
)
|
)
|
||||||
skipFirstTransaction = userTransactionsCount > offset + limit
|
|
||||||
const decay = !(currentPage > 1)
|
|
||||||
const transactions: Transaction[] = []
|
|
||||||
if (userTransactions.length) {
|
|
||||||
if (order === Order.DESC) {
|
|
||||||
userTransactions.reverse()
|
|
||||||
}
|
|
||||||
const involvedUserIds: number[] = []
|
|
||||||
|
|
||||||
|
// find involved users
|
||||||
|
let involvedUserIds: number[] = []
|
||||||
userTransactions.forEach((transaction: dbTransaction) => {
|
userTransactions.forEach((transaction: dbTransaction) => {
|
||||||
involvedUserIds.push(transaction.userId)
|
involvedUserIds.push(transaction.userId)
|
||||||
if (
|
if (transaction.linkedUserId) {
|
||||||
transaction.typeId === TransactionTypeId.SEND ||
|
involvedUserIds.push(transaction.linkedUserId)
|
||||||
transaction.typeId === TransactionTypeId.RECEIVE
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
involvedUserIds.push(transaction.linkedUserId!) // TODO ensure not null properly
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// remove duplicates
|
// remove duplicates
|
||||||
// https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates
|
involvedUserIds = involvedUserIds.filter((value, index, self) => self.indexOf(value) === index)
|
||||||
const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i)
|
// We need to show the name for deleted users for old transactions
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
const involvedUsers = await User.createQueryBuilder()
|
||||||
const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique)
|
.withDeleted()
|
||||||
|
.where('user.id IN (:...userIds)', { involvedUserIds })
|
||||||
|
.getMany()
|
||||||
|
|
||||||
|
const transactions: Transaction[] = []
|
||||||
|
|
||||||
|
// decay transaction
|
||||||
|
if (currentPage === 1 && order === Order.DESC) {
|
||||||
|
const now = new Date()
|
||||||
|
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||||
|
const balance = decay.balance.minus(lastTransaction.balance)
|
||||||
|
|
||||||
|
const decayTransaction = new Transaction()
|
||||||
|
decayTransaction.type = 'decay'
|
||||||
|
decayTransaction.balance = balance
|
||||||
|
// TODO
|
||||||
|
// decayTransaction.decayDuration = decay.duration
|
||||||
|
// decayTransaction.decayStart = decay.start
|
||||||
|
// decayTransaction.decayEnd = decay.end
|
||||||
|
transactions.push(decayTransaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userTransactions.length) {
|
||||||
for (let i = 0; i < userTransactions.length; i++) {
|
for (let i = 0; i < userTransactions.length; i++) {
|
||||||
const userTransaction = userTransactions[i]
|
const userTransaction = userTransactions[i]
|
||||||
const finalTransaction = new Transaction()
|
const finalTransaction = new Transaction()
|
||||||
finalTransaction.transactionId = userTransaction.id
|
finalTransaction.transactionId = userTransaction.id
|
||||||
finalTransaction.date = userTransaction.balanceDate.toISOString()
|
finalTransaction.date = userTransaction.balanceDate.toISOString()
|
||||||
finalTransaction.memo = userTransaction.memo
|
finalTransaction.memo = userTransaction.memo
|
||||||
finalTransaction.totalBalance = roundFloorFrom4(Number(userTransaction.balance))
|
finalTransaction.totalBalance = userTransaction.balance
|
||||||
const previousTransaction = i > 0 ? userTransactions[i - 1] : null
|
finalTransaction.balance = userTransaction.amount
|
||||||
|
|
||||||
if (previousTransaction) {
|
const otherUser = involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
|
||||||
const currentTransaction = userTransaction
|
|
||||||
const decay = calculateDecay(
|
|
||||||
Number(previousTransaction.balance),
|
|
||||||
previousTransaction.balanceDate,
|
|
||||||
currentTransaction.balanceDate,
|
|
||||||
)
|
|
||||||
const balance = Number(previousTransaction.balance) - decay.balance
|
|
||||||
|
|
||||||
if (CONFIG.DECAY_START_TIME < currentTransaction.balanceDate) {
|
|
||||||
finalTransaction.decay = decay
|
|
||||||
finalTransaction.decay.balance = roundFloorFrom4(balance)
|
|
||||||
if (
|
|
||||||
previousTransaction.balanceDate < CONFIG.DECAY_START_TIME &&
|
|
||||||
currentTransaction.balanceDate > CONFIG.DECAY_START_TIME
|
|
||||||
) {
|
|
||||||
finalTransaction.decay.decayStartBlock = (
|
|
||||||
CONFIG.DECAY_START_TIME.getTime() / 1000
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalTransaction.balance = roundFloorFrom4(Number(userTransaction.amount)) // Todo unsafe conversion
|
|
||||||
|
|
||||||
const otherUser = userIndiced.find((u) => u.id === userTransaction.linkedUserId)
|
|
||||||
switch (userTransaction.typeId) {
|
switch (userTransaction.typeId) {
|
||||||
case TransactionTypeId.CREATION:
|
case TransactionTypeId.CREATION:
|
||||||
finalTransaction.name = 'Gradido Akademie'
|
finalTransaction.name = 'Gradido Akademie'
|
||||||
@ -202,32 +143,8 @@ export class TransactionResolver {
|
|||||||
default:
|
default:
|
||||||
throw new Error('invalid transaction')
|
throw new Error('invalid transaction')
|
||||||
}
|
}
|
||||||
if (i > 0 || !skipFirstTransaction) {
|
|
||||||
transactions.push(finalTransaction)
|
transactions.push(finalTransaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i === userTransactions.length - 1 && decay) {
|
|
||||||
const now = new Date()
|
|
||||||
const decay = calculateDecay(
|
|
||||||
Number(userTransaction.balance),
|
|
||||||
userTransaction.balanceDate,
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
const balance = Number(userTransaction.balance) - decay.balance
|
|
||||||
|
|
||||||
const decayTransaction = new Transaction()
|
|
||||||
decayTransaction.type = 'decay'
|
|
||||||
decayTransaction.balance = roundCeilFrom4(balance)
|
|
||||||
decayTransaction.decayDuration = decay.decayDuration
|
|
||||||
decayTransaction.decayStart = decay.decayStart
|
|
||||||
decayTransaction.decayEnd = decay.decayEnd
|
|
||||||
transactions.push(decayTransaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (order === Order.DESC) {
|
|
||||||
transactions.reverse()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transactionList = new TransactionList()
|
const transactionList = new TransactionList()
|
||||||
@ -244,16 +161,12 @@ export class TransactionResolver {
|
|||||||
} catch (err: any) {}
|
} catch (err: any) {}
|
||||||
|
|
||||||
// get balance
|
// get balance
|
||||||
const balanceEntity = await dbBalance.findOne({ userId: user.id })
|
transactionList.balance = lastTransaction.balance
|
||||||
if (balanceEntity) {
|
transactionList.decayStartBlock = CONFIG.DECAY_START_TIME
|
||||||
const now = new Date()
|
// const now = new Date()
|
||||||
transactionList.balance = roundFloorFrom4(balanceEntity.amount)
|
// TODO this seems duplicated
|
||||||
// TODO: Add a decay object here instead of static data representing the decay.
|
// transactionList.decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||||
transactionList.decay = roundFloorFrom4(
|
// transactionList.decayDate = now.toString()
|
||||||
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
|
|
||||||
)
|
|
||||||
transactionList.decayDate = now.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return transactionList
|
return transactionList
|
||||||
}
|
}
|
||||||
@ -271,7 +184,9 @@ export class TransactionResolver {
|
|||||||
throw new Error('invalid sender public key')
|
throw new Error('invalid sender public key')
|
||||||
}
|
}
|
||||||
// validate amount
|
// validate amount
|
||||||
if (!hasUserAmount(senderUser, 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")
|
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,24 +202,19 @@ export class TransactionResolver {
|
|||||||
throw new Error('invalid recipient public key')
|
throw new Error('invalid recipient public key')
|
||||||
}
|
}
|
||||||
|
|
||||||
const centAmount = Math.round(amount * 10000)
|
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||||
try {
|
try {
|
||||||
const receivedCallDate = new Date()
|
|
||||||
// transaction
|
// transaction
|
||||||
const transactionSend = new dbTransaction()
|
const transactionSend = new dbTransaction()
|
||||||
transactionSend.typeId = TransactionTypeId.SEND
|
transactionSend.typeId = TransactionTypeId.SEND
|
||||||
transactionSend.memo = memo
|
transactionSend.memo = memo
|
||||||
transactionSend.userId = senderUser.id
|
transactionSend.userId = senderUser.id
|
||||||
transactionSend.linkedUserId = recipientUser.id
|
transactionSend.linkedUserId = recipientUser.id
|
||||||
transactionSend.amount = BigInt(centAmount)
|
transactionSend.amount = amount
|
||||||
const sendBalance = await calculateNewBalance(senderUser.id, receivedCallDate, -centAmount)
|
transactionSend.balance = sendBalance
|
||||||
transactionSend.balance = BigInt(Math.trunc(sendBalance))
|
|
||||||
transactionSend.balanceDate = receivedCallDate
|
transactionSend.balanceDate = receivedCallDate
|
||||||
transactionSend.sendSenderFinalBalance = transactionSend.balance
|
|
||||||
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
||||||
|
|
||||||
const transactionReceive = new dbTransaction()
|
const transactionReceive = new dbTransaction()
|
||||||
@ -312,15 +222,13 @@ export class TransactionResolver {
|
|||||||
transactionReceive.memo = memo
|
transactionReceive.memo = memo
|
||||||
transactionReceive.userId = recipientUser.id
|
transactionReceive.userId = recipientUser.id
|
||||||
transactionReceive.linkedUserId = senderUser.id
|
transactionReceive.linkedUserId = senderUser.id
|
||||||
transactionReceive.amount = BigInt(centAmount)
|
transactionReceive.amount = amount
|
||||||
const receiveBalance = await calculateNewBalance(
|
const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate)
|
||||||
recipientUser.id,
|
if (!receiveBalance) {
|
||||||
receivedCallDate,
|
throw new Error('Sender user account corrupted')
|
||||||
centAmount,
|
}
|
||||||
)
|
transactionReceive.balance = receiveBalance
|
||||||
transactionReceive.balance = BigInt(Math.trunc(receiveBalance))
|
|
||||||
transactionReceive.balanceDate = receivedCallDate
|
transactionReceive.balanceDate = receivedCallDate
|
||||||
transactionReceive.sendSenderFinalBalance = transactionSend.balance
|
|
||||||
transactionReceive.linkedTransactionId = transactionSend.id
|
transactionReceive.linkedTransactionId = transactionSend.id
|
||||||
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||||
|
|
||||||
@ -328,17 +236,6 @@ export class TransactionResolver {
|
|||||||
transactionSend.linkedTransactionId = transactionReceive.id
|
transactionSend.linkedTransactionId = transactionReceive.id
|
||||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||||
|
|
||||||
// Update Balance sender
|
|
||||||
await updateStateBalance(senderUser, Math.trunc(sendBalance), receivedCallDate, queryRunner)
|
|
||||||
|
|
||||||
// Update Balance recipient
|
|
||||||
await updateStateBalance(
|
|
||||||
recipientUser,
|
|
||||||
Math.trunc(receiveBalance),
|
|
||||||
receivedCallDate,
|
|
||||||
queryRunner,
|
|
||||||
)
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail'
|
import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail'
|
||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
jest.mock('./sendEMail', () => {
|
jest.mock('./sendEMail', () => {
|
||||||
return {
|
return {
|
||||||
@ -16,7 +17,7 @@ describe('sendTransactionReceivedEmail', () => {
|
|||||||
recipientFirstName: 'Peter',
|
recipientFirstName: 'Peter',
|
||||||
recipientLastName: 'Lustig',
|
recipientLastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
amount: 42.0,
|
amount: new Decimal(42.0),
|
||||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
import { transactionReceived } from './text/transactionReceived'
|
import { transactionReceived } from './text/transactionReceived'
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ export const sendTransactionReceivedEmail = (data: {
|
|||||||
recipientFirstName: string
|
recipientFirstName: string
|
||||||
recipientLastName: string
|
recipientLastName: string
|
||||||
email: string
|
email: string
|
||||||
amount: number
|
amount: Decimal
|
||||||
memo: string
|
memo: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
return sendEMail({
|
return sendEMail({
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
export const transactionReceived = {
|
export const transactionReceived = {
|
||||||
de: {
|
de: {
|
||||||
subject: 'Gradido Überweisung',
|
subject: 'Gradido Überweisung',
|
||||||
@ -7,7 +9,7 @@ export const transactionReceived = {
|
|||||||
recipientFirstName: string
|
recipientFirstName: string
|
||||||
recipientLastName: string
|
recipientLastName: string
|
||||||
email: string
|
email: string
|
||||||
amount: number
|
amount: Decimal
|
||||||
memo: string
|
memo: string
|
||||||
}): string =>
|
}): string =>
|
||||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||||
|
|||||||
@ -9,14 +9,6 @@ export class UserRepository extends Repository<User> {
|
|||||||
.getOneOrFail()
|
.getOneOrFail()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUsersIndiced(userIds: number[]): Promise<User[]> {
|
|
||||||
return this.createQueryBuilder('user')
|
|
||||||
.withDeleted() // We need to show the name for deleted users for old transactions
|
|
||||||
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
|
|
||||||
.where('user.id IN (:...userIds)', { userIds })
|
|
||||||
.getMany()
|
|
||||||
}
|
|
||||||
|
|
||||||
async findBySearchCriteriaPagedFiltered(
|
async findBySearchCriteriaPagedFiltered(
|
||||||
select: string[],
|
select: string[],
|
||||||
searchCriteria: string,
|
searchCriteria: string,
|
||||||
|
|||||||
@ -1,29 +1,30 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
import 'reflect-metadata' // This might be wise to load in a test setup file
|
import 'reflect-metadata' // This might be wise to load in a test setup file
|
||||||
import { decayFormula, calculateDecay } from './decay'
|
import { decayFormula, calculateDecay } from './decay'
|
||||||
|
|
||||||
describe('utils/decay', () => {
|
describe('utils/decay', () => {
|
||||||
describe('decayFormula', () => {
|
describe('decayFormula', () => {
|
||||||
it('has base 0.99999997802044727', () => {
|
it('has base 0.99999997802044727', () => {
|
||||||
const amount = 1.0
|
const amount = new Decimal(1.0)
|
||||||
const seconds = 1
|
const seconds = 1
|
||||||
expect(decayFormula(amount, seconds)).toBe(0.99999997802044727)
|
expect(decayFormula(amount, seconds)).toBe(0.99999997802044727)
|
||||||
})
|
})
|
||||||
// Not sure if the following skiped tests make sence!?
|
// Not sure if the following skiped tests make sence!?
|
||||||
it('has negative decay?', async () => {
|
it('has negative decay?', async () => {
|
||||||
const amount = -1.0
|
const amount = new Decimal(1.0)
|
||||||
const seconds = 1
|
const seconds = 1
|
||||||
expect(await decayFormula(amount, seconds)).toBe(-0.99999997802044727)
|
expect(decayFormula(amount, seconds)).toBe(-0.99999997802044727)
|
||||||
})
|
})
|
||||||
it('has correct backward calculation', async () => {
|
it('has correct backward calculation', async () => {
|
||||||
const amount = 1.0
|
const amount = new Decimal(1.0)
|
||||||
const seconds = -1
|
const seconds = -1
|
||||||
expect(await decayFormula(amount, seconds)).toBe(1.0000000219795533)
|
expect(decayFormula(amount, seconds)).toBe(1.0000000219795533)
|
||||||
})
|
})
|
||||||
// not possible, nodejs hasn't enough accuracy
|
// not possible, nodejs hasn't enough accuracy
|
||||||
it('has correct forward calculation', async () => {
|
it('has correct forward calculation', async () => {
|
||||||
const amount = 1.0 / 0.99999997802044727
|
const amount = new Decimal(1.0).div(0.99999997802044727)
|
||||||
const seconds = 1
|
const seconds = 1
|
||||||
expect(await decayFormula(amount, seconds)).toBe(1.0)
|
expect(decayFormula(amount, seconds)).toBe(1.0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it.skip('has base 0.99999997802044727', async () => {
|
it.skip('has base 0.99999997802044727', async () => {
|
||||||
@ -31,11 +32,11 @@ describe('utils/decay', () => {
|
|||||||
now.setSeconds(1)
|
now.setSeconds(1)
|
||||||
const oneSecondAgo = new Date(now.getTime())
|
const oneSecondAgo = new Date(now.getTime())
|
||||||
oneSecondAgo.setSeconds(0)
|
oneSecondAgo.setSeconds(0)
|
||||||
expect(await calculateDecay(1.0, oneSecondAgo, now)).toBe(0.99999997802044727)
|
expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now)).toBe(0.99999997802044727)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns input amount when from and to is the same', async () => {
|
it('returns input amount when from and to is the same', async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
expect((await calculateDecay(100.0, now, now)).balance).toBe(100.0)
|
expect(calculateDecay(new Decimal(100.0), now, now).balance).toBe(100.0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,45 +1,60 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
import CONFIG from '../config'
|
import CONFIG from '../config'
|
||||||
import { Decay } from '../graphql/model/Decay'
|
|
||||||
|
|
||||||
function decayFormula(amount: number, seconds: number): number {
|
// TODO: externalize all those definitions and functions into an external decay library
|
||||||
return amount * Math.pow(0.99999997802044727, seconds) // This number represents 50% decay a year
|
interface Decay {
|
||||||
|
balance: Decimal
|
||||||
|
decay: Decimal | null
|
||||||
|
start: Date | null
|
||||||
|
end: Date | null
|
||||||
|
duration: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateDecay(amount: number, from: Date, to: Date): Decay {
|
function decayFormula(value: Decimal, seconds: number): Decimal {
|
||||||
|
return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDecay(
|
||||||
|
amount: Decimal,
|
||||||
|
from: Date,
|
||||||
|
to: Date,
|
||||||
|
startBlock: Date = CONFIG.DECAY_START_TIME,
|
||||||
|
): Decay {
|
||||||
const fromMs = from.getTime()
|
const fromMs = from.getTime()
|
||||||
const toMs = to.getTime()
|
const toMs = to.getTime()
|
||||||
const decayStartBlockMs = CONFIG.DECAY_START_TIME.getTime()
|
const startBlockMs = startBlock.getTime()
|
||||||
|
|
||||||
if (toMs < fromMs) {
|
if (toMs < fromMs) {
|
||||||
throw new Error('to < from, reverse decay calculation is invalid')
|
throw new Error('to < from, reverse decay calculation is invalid')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with no decay
|
// Initialize with no decay
|
||||||
const decay = new Decay({
|
const decay: Decay = {
|
||||||
balance: amount,
|
balance: amount,
|
||||||
decayStart: null,
|
decay: null,
|
||||||
decayEnd: null,
|
start: null,
|
||||||
decayDuration: 0,
|
end: null,
|
||||||
decayStartBlock: (decayStartBlockMs / 1000).toString(),
|
duration: null,
|
||||||
})
|
}
|
||||||
|
|
||||||
// decay started after end date; no decay
|
// decay started after end date; no decay
|
||||||
if (decayStartBlockMs > toMs) {
|
if (startBlockMs > toMs) {
|
||||||
return decay
|
return decay
|
||||||
}
|
}
|
||||||
// decay started before start date; decay for full duration
|
// decay started before start date; decay for full duration
|
||||||
else if (decayStartBlockMs < fromMs) {
|
if (startBlockMs < fromMs) {
|
||||||
decay.decayStart = (fromMs / 1000).toString()
|
decay.start = from
|
||||||
decay.decayDuration = (toMs - fromMs) / 1000
|
decay.duration = (toMs - fromMs) / 1000
|
||||||
}
|
}
|
||||||
// decay started between start and end date; decay from decay start till end date
|
// decay started between start and end date; decay from decay start till end date
|
||||||
else {
|
else {
|
||||||
decay.decayStart = (decayStartBlockMs / 1000).toString()
|
decay.start = startBlock
|
||||||
decay.decayDuration = (toMs - decayStartBlockMs) / 1000
|
decay.duration = (toMs - startBlockMs) / 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
decay.decayEnd = (toMs / 1000).toString()
|
decay.end = to
|
||||||
decay.balance = decayFormula(amount, decay.decayDuration)
|
decay.balance = decayFormula(amount, decay.duration)
|
||||||
|
decay.decay = decay.balance.minus(amount)
|
||||||
return decay
|
return decay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 } from './round'
|
|
||||||
|
|
||||||
describe('utils/round', () => {
|
|
||||||
it('roundCeilFrom4', () => {
|
|
||||||
const amount = 11617
|
|
||||||
expect(roundCeilFrom4(amount)).toBe(1.17)
|
|
||||||
})
|
|
||||||
// Not sure if the following skiped tests make sence!?
|
|
||||||
it('roundFloorFrom4', () => {
|
|
||||||
const amount = 11617
|
|
||||||
expect(roundFloorFrom4(amount)).toBe(1.16)
|
|
||||||
})
|
|
||||||
it('roundCeilFrom2', () => {
|
|
||||||
const amount = 1216
|
|
||||||
expect(roundCeilFrom2(amount)).toBe(13)
|
|
||||||
})
|
|
||||||
// not possible, nodejs hasn't enough accuracy
|
|
||||||
it('roundFloorFrom2', () => {
|
|
||||||
const amount = 1216
|
|
||||||
expect(roundFloorFrom2(amount)).toBe(12)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
function roundCeilFrom4(decimal: number): number {
|
|
||||||
return Math.ceil(decimal / 100) / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
function roundFloorFrom4(decimal: number): number {
|
|
||||||
return Math.floor(decimal / 100) / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
function roundCeilFrom2(decimal: number): number {
|
|
||||||
return Math.ceil(decimal / 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
function roundFloorFrom2(decimal: number): number {
|
|
||||||
return Math.floor(decimal / 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 }
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { User as dbUser } from '@entity/User'
|
|
||||||
import { Balance as dbBalance } from '@entity/Balance'
|
|
||||||
import { getRepository } from '@dbTools/typeorm'
|
|
||||||
import { calculateDecay } from './decay'
|
import { calculateDecay } from './decay'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
|
||||||
function isStringBoolean(value: string): boolean {
|
function isStringBoolean(value: string): boolean {
|
||||||
const lowerValue = value.toLowerCase()
|
const lowerValue = value.toLowerCase()
|
||||||
@ -15,14 +14,22 @@ function isHexPublicKey(publicKey: string): boolean {
|
|||||||
return /^[0-9A-Fa-f]{64}$/i.test(publicKey)
|
return /^[0-9A-Fa-f]{64}$/i.test(publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hasUserAmount(user: dbUser, amount: number): Promise<boolean> {
|
async function calculateBalance(
|
||||||
if (amount < 0) return false
|
userId: number,
|
||||||
const balanceRepository = getRepository(dbBalance)
|
amount: Decimal,
|
||||||
const balance = await balanceRepository.findOne({ userId: user.id })
|
time: Date,
|
||||||
if (!balance) return false
|
): Promise<Decimal | null> {
|
||||||
|
if (amount.lessThan(0)) return null
|
||||||
|
|
||||||
const decay = calculateDecay(balance.amount, balance.recordDate, new Date()).balance
|
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||||
return decay > amount
|
if (!lastTransaction) return null
|
||||||
|
|
||||||
|
const accountBalance = calculateDecay(
|
||||||
|
lastTransaction.balance,
|
||||||
|
lastTransaction.balanceDate,
|
||||||
|
time,
|
||||||
|
).balance.add(amount)
|
||||||
|
return accountBalance.greaterThan(0) ? accountBalance : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isHexPublicKey, hasUserAmount, isStringBoolean }
|
export { isHexPublicKey, calculateBalance, isStringBoolean }
|
||||||
|
|||||||
@ -1961,6 +1961,11 @@ debug@^3.2.6, debug@^3.2.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
|
decimal.js-light@^2.5.1:
|
||||||
|
version "2.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
|
||||||
|
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
|
||||||
|
|
||||||
decimal.js@^10.2.1:
|
decimal.js@^10.2.1:
|
||||||
version "10.3.1"
|
version "10.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user