Merge pull request #3508 from gradido/refactor_countOpenPendingTransactions_PendingTransactionState

refactor(federation): move code for checking pending transactions
This commit is contained in:
einhornimmond 2025-06-27 16:43:17 +02:00 committed by GitHub
commit dee23c6069
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 193 additions and 71 deletions

View File

@ -1,11 +1,5 @@
import { registerEnumType } from 'type-graphql'
export enum PendingTransactionState {
NEW = 1,
PENDING = 2,
SETTLED = 3,
REVERTED = 4,
}
import { PendingTransactionState } from 'shared'
registerEnumType(PendingTransactionState, {
name: 'PendingTransactionState', // this one is mandatory

View File

@ -1,5 +1,6 @@
import {
AppDatabase,
countOpenPendingTransactions,
Community as DbCommunity,
PendingTransaction as DbPendingTransaction,
Transaction as dbTransaction,
@ -14,7 +15,7 @@ import { In, IsNull } from 'typeorm'
import { Paginated } from '@arg/Paginated'
import { TransactionSendArgs } from '@arg/TransactionSendArgs'
import { Order } from '@enum/Order'
import { PendingTransactionState } from '@enum/PendingTransactionState'
import { PendingTransactionState } from 'shared'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { Transaction } from '@model/Transaction'
import { TransactionList } from '@model/TransactionList'
@ -68,19 +69,7 @@ export const executeTransaction = async (
try {
logger.info('executeTransaction', amount, memo, sender, recipient)
const openSenderPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: sender.gradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: sender.gradidoID, state: PendingTransactionState.NEW },
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: recipient.gradidoID, state: PendingTransactionState.NEW },
],
})
if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) {
if (await countOpenPendingTransactions([sender.gradidoID, recipient.gradidoID]) > 0) {
throw new LogError(
`There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`,
)

View File

@ -6,6 +6,7 @@ import {
PendingTransactionLoggingView,
CommunityLoggingView,
UserLoggingView,
countOpenPendingTransactions,
} from 'database'
import { Decimal } from 'decimal.js-light'
@ -15,7 +16,7 @@ import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0
import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs'
import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult'
import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory'
import { PendingTransactionState } from '@/graphql/enum/PendingTransactionState'
import { PendingTransactionState } from 'shared'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { LogError } from '@/server/LogError'
@ -53,19 +54,7 @@ export async function processXComPendingSendCoins(
}
)
}
const openSenderPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: sender.gradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: sender.gradidoID, state: PendingTransactionState.NEW },
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: recipientIdentifier, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: recipientIdentifier, state: PendingTransactionState.NEW },
],
})
if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) {
if (await countOpenPendingTransactions([sender.gradidoID, recipientIdentifier]) > 0) {
throw new LogError(
`There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`,
)

View File

@ -7,7 +7,7 @@ import {
} from 'database'
import { Decimal } from 'decimal.js-light'
import { PendingTransactionState } from '@/graphql/enum/PendingTransactionState'
import { PendingTransactionState } from 'shared'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { LogError } from '@/server/LogError'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'

View File

@ -1,14 +1,7 @@
import { PendingTransaction, Transaction } from '../entity'
import { AbstractLoggingView } from './AbstractLogging.view'
import { TransactionLoggingView } from './TransactionLogging.view'
// TODO: move enum into database, maybe rename database
enum PendingTransactionState {
NEW = 1,
PENDING = 2,
SETTLED = 3,
REVERTED = 4,
}
import { PendingTransactionState } from 'shared'
export class PendingTransactionLoggingView extends AbstractLoggingView {
public constructor(private self: PendingTransaction) {

View File

@ -2,5 +2,6 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
export * from './user'
export * from './communities'
export * from './pendingTransactions'
export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries`

View File

@ -0,0 +1,124 @@
import {
PendingTransaction as DbPendingTransaction,
User as DbUser,
UserContact as DbUserContact,
Community as DbCommunity
} from '..'
import { countOpenPendingTransactions } from './pendingTransactions'
import { PendingTransactionState } from 'shared'
import { AppDatabase } from '../AppDatabase'
import { userFactory } from '../seeds/factory/user'
import { pendingTransactionFactory } from '../seeds/factory/pendingTransaction'
import { bibiBloxberg } from '../seeds/users/bibi-bloxberg'
import { peterLustig } from '../seeds/users/peter-lustig'
import { bobBaumeister } from '../seeds/users/bob-baumeister'
import { garrickOllivander } from '../seeds/users/garrick-ollivander'
import { describe, expect, it, beforeAll, afterAll } from 'vitest'
import { createCommunity } from '../seeds/homeCommunity'
import { v4 as uuidv4 } from 'uuid'
import Decimal from 'decimal.js-light'
const db = AppDatabase.getInstance()
beforeAll(async () => {
await db.init()
})
afterAll(async () => {
await db.destroy()
})
describe('countOpenPendingTransactions', () => {
let bibi: DbUser
let peter: DbUser
let bob: DbUser
let garrick: DbUser
beforeAll(async () => {
await DbPendingTransaction.clear()
await DbUser.clear()
await DbUserContact.clear()
await DbCommunity.clear()
await createCommunity(false)
bibi = await userFactory(bibiBloxberg)
peter = await userFactory(peterLustig)
bob = await userFactory(bobBaumeister)
garrick = await userFactory(garrickOllivander)
// Bibi -> Peter
await pendingTransactionFactory(
bibi,
peter,
new Decimal(10),
'Bibi -> Peter new',
PendingTransactionState.NEW
)
await pendingTransactionFactory(
bibi,
peter,
new Decimal(100.01),
'Bibi -> Peter settled',
PendingTransactionState.SETTLED
)
// Peter -> Bibi
await pendingTransactionFactory(
peter,
bibi,
new Decimal(12),
'Peter -> Bibi new',
PendingTransactionState.NEW
)
// Bob -> Peter
await pendingTransactionFactory(
bob,
peter,
new Decimal(17.1),
'Bob -> Peter new',
PendingTransactionState.NEW
)
})
it('should return 0 if called with empty array', async () => {
const count = await countOpenPendingTransactions([])
expect(count).toBe(0)
})
it('should return 0 if called with unknown gradido id', async () => {
const count = await countOpenPendingTransactions([uuidv4()])
expect(count).toBe(0)
})
it('should return 0 if there are no pending transactions for the given user', async () => {
const count = await countOpenPendingTransactions([garrick.gradidoID])
expect(count).toBe(0)
})
it('bibi and peter have two transactions together and peter one additional, should return 3', async () => {
const count = await countOpenPendingTransactions([bibi.gradidoID, peter.gradidoID])
expect(count).toBe(3)
})
it('peter and bob have one transaction together, peter two additional, should return 3', async () => {
const count = await countOpenPendingTransactions([peter.gradidoID, bob.gradidoID])
expect(count).toBe(3)
})
it('peter has three transactions, should return 3', async () => {
const count = await countOpenPendingTransactions([peter.gradidoID])
expect(count).toBe(3)
})
it('bibi has two transactions, should return 2', async () => {
const count = await countOpenPendingTransactions([bibi.gradidoID])
expect(count).toBe(2)
})
it('bob has one transaction, should return 1', async () => {
const count = await countOpenPendingTransactions([bob.gradidoID])
expect(count).toBe(1)
})
})

View File

@ -0,0 +1,18 @@
import { PendingTransaction as DbPendingTransaction } from '../entity'
import { In } from 'typeorm'
import { PendingTransactionState } from 'shared'
/**
* Counts the number of open pending transactions for the given users.
* @param users The users gradidoID to count the pending transactions for
* @returns The number of open pending transactions
*/
export async function countOpenPendingTransactions(users: string[]): Promise<number> {
const count = await DbPendingTransaction.count({
where: [
{ userGradidoID: In(users), state: PendingTransactionState.NEW },
{ linkedUserGradidoID: In(users), state: PendingTransactionState.NEW },
],
})
return count
}

View File

@ -3,13 +3,12 @@ import { AppDatabase } from '../AppDatabase'
import { aliasExists, findUserByIdentifier } from './user'
import { userFactory } from '../seeds/factory/user'
import { bibiBloxberg } from '../seeds/users/bibi-bloxberg'
import { describe, expect, it, beforeAll, afterAll } from 'vitest'
import { describe, expect, it, beforeAll, afterAll, beforeEach, } from 'vitest'
import { createCommunity } from '../seeds/homeCommunity'
import { peterLustig } from '../seeds/users/peter-lustig'
import { bobBaumeister } from '../seeds/users/bob-baumeister'
import { getLogger, printLogs, clearLogs } from '../../../config-schema/test/testSetup.vitest'
import { LOG4JS_QUERIES_CATEGORY_NAME } from '.'
import { beforeEach } from 'node:test'
const db = AppDatabase.getInstance()
const userIdentifierLoggerName = `${LOG4JS_QUERIES_CATEGORY_NAME}.user.findUserByIdentifier`

View File

@ -0,0 +1,23 @@
import { User as DbUser, PendingTransaction as DbPendingTransaction } from '../..'
import { PendingTransactionState } from 'shared'
import { Decimal } from 'decimal.js-light'
export async function pendingTransactionFactory(
sender: DbUser,
receiver: DbUser,
amount: Decimal,
memo: string,
state: PendingTransactionState,
) {
const pendingTransaction = new DbPendingTransaction()
pendingTransaction.state = state
pendingTransaction.memo = memo
pendingTransaction.amount = amount
pendingTransaction.userId = sender.id
pendingTransaction.userGradidoID = sender.gradidoID
pendingTransaction.userCommunityUuid = sender.communityUuid!
pendingTransaction.linkedUserId = receiver.id
pendingTransaction.linkedUserGradidoID = receiver.gradidoID
pendingTransaction.linkedUserCommunityUuid = receiver.communityUuid!
await pendingTransaction.save()
}

View File

@ -14,7 +14,9 @@ export const userFactory = async (user: UserInterface): Promise<User> => {
let dbUser = new User()
dbUser.firstName = user.firstName ?? ''
dbUser.lastName = user.lastName ?? ''
dbUser.alias = user.alias ?? ''
if (user.alias) {
dbUser.alias = user.alias
}
dbUser.language = user.language ?? 'en'
dbUser.createdAt = user.createdAt ?? new Date()
dbUser.deletedAt = user.deletedAt ?? null

View File

@ -1,11 +1,5 @@
import { registerEnumType } from 'type-graphql'
export enum PendingTransactionState {
NEW = 1,
PENDING = 2,
SETTLED = 3,
REVERTED = 4,
}
import { PendingTransactionState } from 'shared'
registerEnumType(PendingTransactionState, {
name: 'PendingTransactionState', // this one is mandatory

View File

@ -10,7 +10,7 @@ import Decimal from 'decimal.js-light'
import { getLogger } from 'log4js'
import { Arg, Mutation, Resolver } from 'type-graphql'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { PendingTransactionState } from 'shared'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { SendCoinsArgsLoggingView } from '../logger/SendCoinsArgsLogging.view'
import { SendCoinsArgs } from '../model/SendCoinsArgs'
@ -20,7 +20,7 @@ import { calculateRecipientBalance } from '../util/calculateRecipientBalance'
import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTransaction'
import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction'
import { storeForeignUser } from '../util/storeForeignUser'
import { countOpenPendingTransactions } from 'database'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.SendCoinsResolver`)
@Resolver()
@ -56,19 +56,8 @@ export class SendCoinsResolver {
homeCom.name,
)
}
const openSenderPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: args.senderUserUuid, state: PendingTransactionState.NEW },
],
})
const openReceiverPendingTx = await DbPendingTransaction.count({
where: [
{ userGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW },
{ linkedUserGradidoID: receiverUser.gradidoID, state: PendingTransactionState.NEW },
],
})
if (openSenderPendingTx > 0 || openReceiverPendingTx > 0) {
if (await countOpenPendingTransactions([args.senderUserUuid, receiverUser.gradidoID]) > 0) {
throw new LogError(
`There exist still ongoing 'Pending-Transactions' for the involved users on receiver-side!`,
)

View File

@ -10,7 +10,7 @@ import {
Transaction as dbTransaction,
} from 'database'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { PendingTransactionState } from 'shared'
import { LogError } from '@/server/LogError'
import { getLogger } from 'log4js'

View File

@ -9,7 +9,7 @@ import {
UserLoggingView,
Transaction as dbTransaction,
} from 'database'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { PendingTransactionState } from 'shared'
import { LogError } from '@/server/LogError'

View File

@ -0,0 +1,6 @@
export enum PendingTransactionState {
NEW = 1,
PENDING = 2,
SETTLED = 3,
REVERTED = 4,
}

View File

@ -2,3 +2,4 @@ export * from './RoleNames'
export * from './UserContactType'
export * from './PasswordEncryptionType'
export * from './OptInType'
export * from './PendingTransactionState'