diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 50f9b0414..f60ab45d0 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -16,6 +16,7 @@ import { redeemTransactionLink, createContribution, updateContribution, + createTransactionLink, } from '@/seeds/graphql/mutations' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' @@ -24,6 +25,7 @@ import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import Decimal from 'decimal.js-light' import { GraphQLError } from 'graphql' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { logger } from '@test/testSetup' // mock semaphore to allow use fake timers jest.mock('@/util/TRANSACTIONS_LOCK') @@ -50,7 +52,75 @@ afterAll(async () => { }) describe('TransactionLinkResolver', () => { + describe('createTransactionLink', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + it('throws error when amount is zero', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: 0, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Amount must be a positive number')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0)) + }) + + it('throws error when amount is negative', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: -10, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Amount must be a positive number')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10)) + }) + + it('throws error when user has not enough GDD', async () => { + jest.clearAllMocks() + await expect( + mutate({ + mutation: createTransactionLink, + variables: { + amount: 1001, + memo: 'Test', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('User has not enough GDD')], + }) + }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number)) + }) + }) + describe('redeemTransactionLink', () => { + afterAll(async () => { + await cleanDB() + resetToken() + }) + describe('contributionLink', () => { describe('input not valid', () => { beforeAll(async () => { @@ -61,6 +131,7 @@ describe('TransactionLinkResolver', () => { }) it('throws error when link does not exists', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -69,16 +140,26 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'No contribution link found to given code', + 'CL-123456', + ) + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('No contribution link found to given code'), + ) + }) + + const now = new Date() + const validFrom = new Date(now.getFullYear() + 1, 0, 1) + it('throws error when link is not valid yet', async () => { - const now = new Date() + jest.clearAllMocks() const { data: { createContributionLink: contributionLink }, } = await mutate({ @@ -88,7 +169,7 @@ describe('TransactionLinkResolver', () => { name: 'Daily Contribution Link', memo: 'Thank you for contribute daily to the community', cycle: 'DAILY', - validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(), + validFrom: validFrom.toISOString(), validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(), maxAmountPerMonth: new Decimal(200), maxPerCycle: 1, @@ -102,16 +183,21 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link not valid yet', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) await resetEntity(DbContributionLink) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom) + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link is not valid yet'), + ) + }) + it('throws error when contributionLink cycle is invalid', async () => { + jest.clearAllMocks() const now = new Date() const { data: { createContributionLink: contributionLink }, @@ -136,17 +222,22 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link has unknown cycle', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) await resetEntity(DbContributionLink) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID') + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link has unknown cycle'), + ) + }) + + const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0) it('throws error when link is no longer valid', async () => { - const now = new Date() + jest.clearAllMocks() const { data: { createContributionLink: contributionLink }, } = await mutate({ @@ -157,7 +248,7 @@ describe('TransactionLinkResolver', () => { memo: 'Thank you for contribute daily to the community', cycle: 'DAILY', validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(), - validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(), + validTo: validTo.toISOString(), maxAmountPerMonth: new Decimal(200), maxPerCycle: 1, }, @@ -170,14 +261,18 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link is no longer valid', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) await resetEntity(DbContributionLink) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo) + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link is no longer valid'), + ) + }) }) // TODO: have this test separated into a transactionLink and a contributionLink part @@ -250,6 +345,7 @@ describe('TransactionLinkResolver', () => { }) it('does not allow the user to redeem the contribution link', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -258,13 +354,18 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error( + 'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.', + ), + ) + }) }) describe('user has no pending contributions that would not allow to redeem the link', () => { @@ -301,6 +402,7 @@ describe('TransactionLinkResolver', () => { }) it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -309,14 +411,17 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link already redeemed today'), + ) + }) + describe('after one day', () => { beforeAll(async () => { jest.useFakeTimers() @@ -349,6 +454,7 @@ describe('TransactionLinkResolver', () => { }) it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + jest.clearAllMocks() await expect( mutate({ mutation: redeemTransactionLink, @@ -357,33 +463,65 @@ describe('TransactionLinkResolver', () => { }, }), ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', - ), - ], + errors: [new GraphQLError('Creation from contribution link was not successful')], }) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Creation from contribution link was not successful', + new Error('Contribution link already redeemed today'), + ) + }) }) }) }) }) + }) - describe('transaction links list', () => { - const variables = { - userId: 1, // dummy, may be replaced - filters: null, - currentPage: 1, - pageSize: 5, - } + describe('listTransactionLinksAdmin', () => { + const variables = { + userId: 1, // dummy, may be replaced + filters: null, + currentPage: 1, + pageSize: 5, + } - // TODO: there is a test not cleaning up after itself! Fix it! - beforeAll(async () => { - await cleanDB() - resetToken() + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) }) + }) + + describe('authenticated', () => { + describe('without admin rights', () => { + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) - describe('unauthenticated', () => { it('returns an error', async () => { await expect( query({ @@ -398,22 +536,40 @@ describe('TransactionLinkResolver', () => { }) }) - describe('authenticated', () => { - describe('without admin rights', () => { - beforeAll(async () => { - user = await userFactory(testEnv, bibiBloxberg) - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - }) + describe('with admin rights', () => { + beforeAll(async () => { + // admin 'peter@lustig.de' has to exists for 'creationFactory' + await userFactory(testEnv, peterLustig) - afterAll(async () => { - await cleanDB() - resetToken() - }) + user = await userFactory(testEnv, bibiBloxberg) + variables.userId = user.id + variables.pageSize = 25 + // bibi needs GDDs + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + // bibis transaktion links + const bibisTransaktionLinks = transactionLinks.filter( + (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', + ) + for (let i = 0; i < bibisTransaktionLinks.length; i++) { + await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) + } - it('returns an error', async () => { + // admin: only now log in + await mutate({ + mutation: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + describe('without any filters', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { await expect( query({ query: listTransactionLinksAdmin, @@ -421,219 +577,169 @@ describe('TransactionLinkResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, }), ) }) }) - describe('with admin rights', () => { - beforeAll(async () => { - // admin 'peter@lustig.de' has to exists for 'creationFactory' - await userFactory(testEnv, peterLustig) - - user = await userFactory(testEnv, bibiBloxberg) - variables.userId = user.id - variables.pageSize = 25 - // bibi needs GDDs - const bibisCreation = creations.find( - (creation) => creation.email === 'bibi@bloxberg.de', + describe('all filters are null', () => { + it('finds 6 open transaction links and no deleted or redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: null, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.not.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), ) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await creationFactory(testEnv, bibisCreation!) - // bibis transaktion links - const bibisTransaktionLinks = transactionLinks.filter( - (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', + }) + }) + + describe('filter with deleted', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), ) - for (let i = 0; i < bibisTransaktionLinks.length; i++) { - await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) - } - - // admin: only now log in - await mutate({ - mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, - }) }) + }) - afterAll(async () => { - await cleanDB() - resetToken() + describe('filter by expired', () => { + it('finds 5 open transaction links, 1 expired, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withExpired: true, + }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 7, + linkList: expect.arrayContaining([ + expect.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), + }, + }, + }), + ) }) + }) - describe('without any filters', () => { - it('finds 6 open transaction links and no deleted or redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.not.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, + // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory + describe.skip('filter by redeemed', () => { + it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { + await expect( + query({ + query: listTransactionLinksAdmin, + variables: { + ...variables, + filters: { + withDeleted: null, + withExpired: null, + withRedeemed: true, }, - }), - ) - }) - }) - - describe('all filters are null', () => { - it('finds 6 open transaction links and no deleted or redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: null, - withExpired: null, - withRedeemed: null, - }, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listTransactionLinksAdmin: { + linkCount: 6, + linkList: expect.arrayContaining([ + expect.not.objectContaining({ + memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', + createdAt: expect.any(String), + }), + expect.objectContaining({ + memo: 'Yeah, eingelöst!', + redeemedAt: expect.any(String), + redeemedBy: expect.any(Number), + }), + expect.not.objectContaining({ + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: expect.any(String), + }), + ]), }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.not.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - describe('filter with deleted', () => { - it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 7, - linkList: expect.arrayContaining([ - expect.not.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - describe('filter by expired', () => { - it('finds 5 open transaction links, 1 expired, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withExpired: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 7, - linkList: expect.arrayContaining([ - expect.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.not.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) - }) - - // TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory - describe.skip('filter by redeemed', () => { - it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => { - await expect( - query({ - query: listTransactionLinksAdmin, - variables: { - ...variables, - filters: { - withDeleted: null, - withExpired: null, - withRedeemed: true, - }, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - listTransactionLinksAdmin: { - linkCount: 6, - linkList: expect.arrayContaining([ - expect.not.objectContaining({ - memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(', - createdAt: expect.any(String), - }), - expect.objectContaining({ - memo: 'Yeah, eingelöst!', - redeemedAt: expect.any(String), - redeemedBy: expect.any(Number), - }), - expect.not.objectContaining({ - memo: 'Da habe ich mich wohl etwas übernommen.', - deletedAt: expect.any(String), - }), - ]), - }, - }, - }), - ) - }) + }, + }), + ) }) }) }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index b3376c65f..4647dde60 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -32,6 +32,7 @@ import { getUserCreation, validateContribution } from './util/creations' import { executeTransaction } from './TransactionResolver' import QueryLinkResult from '@union/QueryLinkResult' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import LogError from '@/server/LogError' import { getLastTransaction } from './util/getLastTransaction' @@ -65,12 +66,16 @@ export class TransactionLinkResolver { const createdDate = new Date() const validUntil = transactionLinkExpireDate(createdDate) + if (amount.lessThanOrEqualTo(0)) { + throw new LogError('Amount must be a positive number', amount) + } + 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") + throw new LogError('User has not enough GDD', user.id) } const transactionLink = DbTransactionLink.create() @@ -186,24 +191,15 @@ export class TransactionLinkResolver { .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 to given code: ${code}`) + throw new LogError('No contribution link found to given code', code) } 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') + throw new LogError('Contribution link is not valid yet', contributionLink.validFrom) } if (contributionLink.validTo) { if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) { - logger.error( - 'contribution link is no longer valid. Valid to: ', - contributionLink.validTo, - ) - throw new Error('Contribution link is no longer valid') + throw new LogError('Contribution link is no longer valid', contributionLink.validTo) } } let alreadyRedeemed: DbContribution | undefined @@ -219,11 +215,7 @@ export class TransactionLinkResolver { }) .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') + throw new LogError('Contribution link already redeemed', user.id) } break } @@ -248,17 +240,12 @@ export class TransactionLinkResolver { ) .getOne() if (alreadyRedeemed) { - logger.error( - 'contribution link with rule DAILY already redeemed by user with id', - user.id, - ) - throw new Error('Contribution link already redeemed today') + throw new LogError('Contribution link already redeemed today', user.id) } break } default: { - logger.error('contribution link has unknown cycle', contributionLink.cycle) - throw new Error('Contribution link has unknown cycle') + throw new LogError('Contribution link has unknown cycle', contributionLink.cycle) } } @@ -308,8 +295,7 @@ export class TransactionLinkResolver { 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}`) + throw new LogError('Creation from contribution link was not successful', e) } finally { await queryRunner.release() }