diff --git a/backend/src/graphql/enum/PendingTransactionState.ts b/backend/src/graphql/enum/PendingTransactionState.ts index d89b0b0eb..e59f3fd7d 100644 --- a/backend/src/graphql/enum/PendingTransactionState.ts +++ b/backend/src/graphql/enum/PendingTransactionState.ts @@ -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 diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 9dedb395f..67dcc4432 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -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!`, ) diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index 4cbf9b570..2d85a6c1a 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -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!`, ) diff --git a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts index a10e982b3..2d532113c 100644 --- a/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts +++ b/backend/src/graphql/resolver/util/settlePendingSenderTransaction.ts @@ -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' diff --git a/database/src/logging/PendingTransactionLogging.view.ts b/database/src/logging/PendingTransactionLogging.view.ts index 9cf27be88..fad2d8f56 100644 --- a/database/src/logging/PendingTransactionLogging.view.ts +++ b/database/src/logging/PendingTransactionLogging.view.ts @@ -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) { diff --git a/database/src/queries/index.ts b/database/src/queries/index.ts index c244a4af0..9acf80871 100644 --- a/database/src/queries/index.ts +++ b/database/src/queries/index.ts @@ -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` diff --git a/database/src/queries/pendingTransactions.test.ts b/database/src/queries/pendingTransactions.test.ts new file mode 100644 index 000000000..72deaac71 --- /dev/null +++ b/database/src/queries/pendingTransactions.test.ts @@ -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) + }) +}) diff --git a/database/src/queries/pendingTransactions.ts b/database/src/queries/pendingTransactions.ts new file mode 100644 index 000000000..44bf63f82 --- /dev/null +++ b/database/src/queries/pendingTransactions.ts @@ -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 { + const count = await DbPendingTransaction.count({ + where: [ + { userGradidoID: In(users), state: PendingTransactionState.NEW }, + { linkedUserGradidoID: In(users), state: PendingTransactionState.NEW }, + ], + }) + return count +} \ No newline at end of file diff --git a/database/src/queries/user.test.ts b/database/src/queries/user.test.ts index 1b0008c94..8cb95e0ac 100644 --- a/database/src/queries/user.test.ts +++ b/database/src/queries/user.test.ts @@ -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` diff --git a/database/src/seeds/factory/pendingTransaction.ts b/database/src/seeds/factory/pendingTransaction.ts new file mode 100644 index 000000000..2e8c7d256 --- /dev/null +++ b/database/src/seeds/factory/pendingTransaction.ts @@ -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() +} diff --git a/database/src/seeds/factory/user.ts b/database/src/seeds/factory/user.ts index 293ee4bb3..3772fe66d 100644 --- a/database/src/seeds/factory/user.ts +++ b/database/src/seeds/factory/user.ts @@ -14,7 +14,9 @@ export const userFactory = async (user: UserInterface): Promise => { 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 diff --git a/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts index d89b0b0eb..e59f3fd7d 100644 --- a/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts +++ b/federation/src/graphql/api/1_0/enum/PendingTransactionState.ts @@ -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 diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index 933c9d1eb..35850fc3e 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -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!`, ) diff --git a/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts index 685a7b9ff..9b311be30 100644 --- a/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts +++ b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts @@ -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' diff --git a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts index 49e8ca203..6396174bc 100644 --- a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts +++ b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts @@ -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' diff --git a/shared/src/enum/PendingTransactionState.ts b/shared/src/enum/PendingTransactionState.ts new file mode 100644 index 000000000..644fdc8dc --- /dev/null +++ b/shared/src/enum/PendingTransactionState.ts @@ -0,0 +1,6 @@ +export enum PendingTransactionState { + NEW = 1, + PENDING = 2, + SETTLED = 3, + REVERTED = 4, +} diff --git a/shared/src/enum/index.ts b/shared/src/enum/index.ts index efd722624..adadf38ce 100644 --- a/shared/src/enum/index.ts +++ b/shared/src/enum/index.ts @@ -2,3 +2,4 @@ export * from './RoleNames' export * from './UserContactType' export * from './PasswordEncryptionType' export * from './OptInType' +export * from './PendingTransactionState'