From 4e72aafe4d787287429d8e90d0cfe91d68096b97 Mon Sep 17 00:00:00 2001 From: elweyn Date: Sun, 18 Sep 2022 11:22:25 +0200 Subject: [PATCH 01/21] Change the query so that we only look on the contributions table. --- .../src/graphql/resolver/util/creations.ts | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index ad15ebec6..3a6a14fda 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -43,39 +43,54 @@ export const getUserCreations = async ( const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day' logger.trace('getUserCreations dateFilter', dateFilter) - const unionString = includePending - ? ` - UNION - SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions - WHERE user_id IN (${ids.toString()}) - AND contribution_date >= ${dateFilter} - AND confirmed_at IS NULL AND deleted_at IS NULL` - : '' - - const unionQuery = await queryRunner.manager.query(` - SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM - (SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions - WHERE user_id IN (${ids.toString()}) - AND type_id = ${TransactionTypeId.CREATION} - AND creation_date >= ${dateFilter} - ${unionString}) AS result - GROUP BY month, userId - ORDER BY date DESC - `) - + /** + SELECT MONTH(contribution_date) as month, user_id, created_at, sum(amount), confirmed_at, deleted_at + FROM `contributions` + where user_id = 776 + and contribution_date >= last_day(curdate() - interval 3 month) + interval 1 day + and deleted_at IS NULL + if(!includePending) and confirmed_at IS NOT NULL + group by month, user_id; + */ + const bookedCreationQuery = queryRunner.manager + .createQueryBuilder(Contribution, 'c') + .select('month(contribution_date)', 'month') + .addSelect('user_id', 'userId') + .addSelect('sum(amount)', 'sum') + .where(`user_id in (${ids.toString()})`) + .andWhere(`contribution_date >= ${dateFilter}`) + .andWhere('deleted_at IS NULL') + .groupBy('month') + .addGroupBy('userId') + if (!includePending) { + bookedCreationQuery.andWhere('confirmed_at IS NOT NULL') + } + const bookedCreation = await bookedCreationQuery.getRawMany() + // eslint-disable-next-line no-console + console.log('openCreation', bookedCreation) await queryRunner.release() return ids.map((id) => { return { id, creations: months.map((month) => { - const creation = unionQuery.find( - (raw: { month: string; id: string; creation: number[] }) => - parseInt(raw.month) === month && parseInt(raw.id) === id, + const creation = bookedCreation.find( + (raw: { month: string; userId: string; creation: number[] }) => + parseInt(raw.month) === month && parseInt(raw.userId) === id, ) return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) }), } + // const creations = months.map((month) => { + // const creation = openCreation.find( + // (raw: { month: string; userId: string; creation: number[] }) => + // parseInt(raw.month) === month && parseInt(raw.userId) === id, + // ) + // return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) + // }) + // // eslint-disable-next-line no-console + // console.log('id: ', id, 'creations: ', creations.toString()) + // return { id, creations } }) } From e097c30003ce5162616e5df3fb6294dfaeeaa710 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 21 Sep 2022 11:47:24 +0200 Subject: [PATCH 02/21] Add denied_at is null to query, change variable name of query. --- backend/src/graphql/resolver/util/creations.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 3a6a14fda..6135d282a 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -1,4 +1,3 @@ -import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { backendLogger as logger } from '@/server/logger' import { getConnection } from '@dbTools/typeorm' import { Contribution } from '@entity/Contribution' @@ -49,10 +48,11 @@ export const getUserCreations = async ( where user_id = 776 and contribution_date >= last_day(curdate() - interval 3 month) + interval 1 day and deleted_at IS NULL + and denied_at IS NULL if(!includePending) and confirmed_at IS NOT NULL group by month, user_id; */ - const bookedCreationQuery = queryRunner.manager + const sumAmountContributionPerUserAndLast3MonthQuery = queryRunner.manager .createQueryBuilder(Contribution, 'c') .select('month(contribution_date)', 'month') .addSelect('user_id', 'userId') @@ -60,21 +60,22 @@ export const getUserCreations = async ( .where(`user_id in (${ids.toString()})`) .andWhere(`contribution_date >= ${dateFilter}`) .andWhere('deleted_at IS NULL') + .andWhere('denied_at IS NULL') .groupBy('month') .addGroupBy('userId') if (!includePending) { - bookedCreationQuery.andWhere('confirmed_at IS NOT NULL') + sumAmountContributionPerUserAndLast3MonthQuery.andWhere('confirmed_at IS NOT NULL') } - const bookedCreation = await bookedCreationQuery.getRawMany() - // eslint-disable-next-line no-console - console.log('openCreation', bookedCreation) + const sumAmountContributionPerUserAndLast3Month = + await sumAmountContributionPerUserAndLast3MonthQuery.getRawMany() + await queryRunner.release() return ids.map((id) => { return { id, creations: months.map((month) => { - const creation = bookedCreation.find( + const creation = sumAmountContributionPerUserAndLast3Month.find( (raw: { month: string; userId: string; creation: number[] }) => parseInt(raw.month) === month && parseInt(raw.userId) === id, ) From 2ca9945a1b1675ffe402b9734d225840bdb6ce5e Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 28 Sep 2022 11:09:14 +0200 Subject: [PATCH 03/21] added missing events --- backend/src/graphql/resolver/AdminResolver.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index e9ee0b55b..4bf2794dc 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -66,6 +66,8 @@ import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessage } from '@model/ContributionMessage' import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail' +import { eventProtocol } from '@/event/EventProtocolEmitter' +import { Event, EventContributionConfirm, EventContributionCreate, EventContributionLinkDefine, EventSendConfirmationEmail } from '@/event/Event' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -241,6 +243,8 @@ export class AdminResolver { logger.error('Contribution could not be saved, Email is not activated') throw new Error('Contribution could not be saved, Email is not activated') } + + const event = new Event() const moderator = getUser(context) logger.trace('moderator: ', moderator.id) const creations = await getUserCreation(emailContact.userId) @@ -260,6 +264,13 @@ export class AdminResolver { logger.trace('contribution to save', contribution) await Contribution.save(contribution) + + const eventCreateContribution = new EventContributionCreate() + eventCreateContribution.userId = moderator.id + eventCreateContribution.amount = amount + eventCreateContribution.contributionId = contribution.id + await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution)) + return getUserCreation(emailContact.userId) } @@ -495,6 +506,13 @@ export class AdminResolver { contributionAmount: contribution.amount, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) + + const event = new Event() + const eventContributionConfirm = new EventContributionConfirm() + eventContributionConfirm.xUserId = user.id + eventContributionConfirm.amount = contribution.amount + eventContributionConfirm.contributionId = contribution.id + await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm)) } catch (e) { await queryRunner.rollbackTransaction() logger.error(`Creation was not successful: ${e}`) @@ -558,6 +576,13 @@ export class AdminResolver { // In case EMails are disabled log the activation link for the user if (!emailSent) { logger.info(`Account confirmation link: ${activationLink}`) + } else { + const event = new Event() + const eventSendConfirmationEmail = new EventSendConfirmationEmail() + eventSendConfirmationEmail.userId = user.id + await eventProtocol.writeEvent( + event.setEventSendConfirmationEmail(eventSendConfirmationEmail), + ) } return true @@ -660,6 +685,13 @@ export class AdminResolver { dbContributionLink.maxAmountPerMonth = maxAmountPerMonth dbContributionLink.maxPerCycle = maxPerCycle await dbContributionLink.save() + + const event = new Event() + const eventContributionLinkDefine = new EventContributionLinkDefine() + await eventProtocol.writeEvent( + event.setEventContributionLinkDefine(eventContributionLinkDefine), + ) + logger.debug(`createContributionLink successful!`) return new ContributionLink(dbContributionLink) } From 33eeab344f6e2b479452ab51599b17d5eb045026 Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 28 Sep 2022 11:14:16 +0200 Subject: [PATCH 04/21] added missing error logs --- backend/src/graphql/resolver/AdminResolver.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 4bf2794dc..660f5ab31 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -148,11 +148,13 @@ export class AdminResolver { const user = await dbUser.findOne({ id: userId }) // user exists ? if (!user) { + logger.error(`Could not find user with userId: ${userId}`) throw new Error(`Could not find user with userId: ${userId}`) } // administrator user changes own role? const moderatorUser = getUser(context) if (moderatorUser.id === userId) { + logger.error('Administrator can not change his own role!') throw new Error('Administrator can not change his own role!') } // change isAdmin @@ -161,6 +163,7 @@ export class AdminResolver { if (isAdmin === true) { user.isAdmin = new Date() } else { + logger.error('User is already a usual user!') throw new Error('User is already a usual user!') } break @@ -168,6 +171,7 @@ export class AdminResolver { if (isAdmin === false) { user.isAdmin = null } else { + logger.error('User is already admin!') throw new Error('User is already admin!') } break @@ -186,11 +190,13 @@ export class AdminResolver { const user = await dbUser.findOne({ id: userId }) // user exists ? if (!user) { + logger.error(`Could not find user with userId: ${userId}`) throw new Error(`Could not find user with userId: ${userId}`) } // moderator user disabled own account? const moderatorUser = getUser(context) if (moderatorUser.id === userId) { + logger.error('Moderator can not delete his own account!') throw new Error('Moderator can not delete his own account!') } // soft-delete user @@ -204,9 +210,11 @@ export class AdminResolver { async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise { const user = await dbUser.findOne({ id: userId }, { withDeleted: true }) if (!user) { + logger.error(`Could not find user with userId: ${userId}`) throw new Error(`Could not find user with userId: ${userId}`) } if (!user.deletedAt) { + logger.error('User is not deleted') throw new Error('User is not deleted') } await user.recover() @@ -781,9 +789,11 @@ export class AdminResolver { relations: ['user'], }) if (!contribution) { + logger.error('Contribution not found') throw new Error('Contribution not found') } if (contribution.userId === user.id) { + logger.error('Admin can not answer on own contribution') throw new Error('Admin can not answer on own contribution') } if (!contribution.user.emailContact) { From 0f1f9baa8dd0f836a0f1112a98865118968f5647 Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 28 Sep 2022 11:57:49 +0200 Subject: [PATCH 05/21] added tests for events --- .../graphql/resolver/AdminResolver.test.ts | 27 ++++++++++++++++++ backend/src/graphql/resolver/AdminResolver.ts | 28 +++++++++---------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index b1b4e469e..c132cb10f 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -41,6 +41,8 @@ import { Contribution } from '@entity/Contribution' import { Transaction as DbTransaction } from '@entity/Transaction' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' +import { EventProtocol } from '@entity/EventProtocol' +import { EventProtocolType } from '@/event/EventProtocolType' // mock account activation email to avoid console spam jest.mock('@/mailer/sendAccountActivationEmail', () => { @@ -1037,6 +1039,15 @@ describe('AdminResolver', () => { }), ) }) + + it('stores the create contribution event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.CONTRIBUTION_CREATE, + userId: admin.id, + }), + ) + }) }) describe('second creation surpasses the available amount ', () => { @@ -1451,6 +1462,14 @@ describe('AdminResolver', () => { ) }) + it('stores the contribution confirm event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.CONTRIBUTION_CONFIRM, + }), + ) + }) + it('creates a transaction', async () => { const transaction = await DbTransaction.find() expect(transaction[0].amount.toString()).toBe('450') @@ -1475,6 +1494,14 @@ describe('AdminResolver', () => { }), ) }) + + it('stores the send confirmation email event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.SEND_CONFIRMATION_EMAIL, + }), + ) + }) }) describe('confirm two creations one after the other quickly', () => { diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 660f5ab31..b03da9dc4 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -67,7 +67,12 @@ import { ContributionMessage } from '@model/ContributionMessage' import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail' import { eventProtocol } from '@/event/EventProtocolEmitter' -import { Event, EventContributionConfirm, EventContributionCreate, EventContributionLinkDefine, EventSendConfirmationEmail } from '@/event/Event' +import { + Event, + EventContributionConfirm, + EventContributionCreate, + EventSendConfirmationEmail, +} from '@/event/Event' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -514,13 +519,6 @@ export class AdminResolver { contributionAmount: contribution.amount, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) - - const event = new Event() - const eventContributionConfirm = new EventContributionConfirm() - eventContributionConfirm.xUserId = user.id - eventContributionConfirm.amount = contribution.amount - eventContributionConfirm.contributionId = contribution.id - await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm)) } catch (e) { await queryRunner.rollbackTransaction() logger.error(`Creation was not successful: ${e}`) @@ -528,6 +526,13 @@ export class AdminResolver { } finally { await queryRunner.release() } + + const event = new Event() + const eventContributionConfirm = new EventContributionConfirm() + eventContributionConfirm.userId = user.id + eventContributionConfirm.amount = contribution.amount + eventContributionConfirm.contributionId = contribution.id + await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm)) return true } @@ -693,13 +698,6 @@ export class AdminResolver { dbContributionLink.maxAmountPerMonth = maxAmountPerMonth dbContributionLink.maxPerCycle = maxPerCycle await dbContributionLink.save() - - const event = new Event() - const eventContributionLinkDefine = new EventContributionLinkDefine() - await eventProtocol.writeEvent( - event.setEventContributionLinkDefine(eventContributionLinkDefine), - ) - logger.debug(`createContributionLink successful!`) return new ContributionLink(dbContributionLink) } From a756f0a4cc7c078e106f5c2515864d4196703cdf Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 28 Sep 2022 12:47:00 +0200 Subject: [PATCH 06/21] all tests completed --- .../graphql/resolver/AdminResolver.test.ts | 189 ++++++++++++++++++ .../src/graphql/resolver/util/creations.ts | 2 +- 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index c132cb10f..6576a7d04 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -43,6 +43,7 @@ import { ContributionLink as DbContributionLink } from '@entity/ContributionLink import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { EventProtocol } from '@entity/EventProtocol' import { EventProtocolType } from '@/event/EventProtocolType' +import { logger } from '@test/testSetup' // mock account activation email to avoid console spam jest.mock('@/mailer/sendAccountActivationEmail', () => { @@ -144,6 +145,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`) + }) }) describe('change role with success', () => { @@ -196,6 +201,9 @@ describe('AdminResolver', () => { }), ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Administrator can not change his own role!') + }) }) describe('user has already role to be set', () => { @@ -213,6 +221,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('User is already admin!') + }) }) describe('to usual user', () => { @@ -229,6 +241,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('User is already a usual user!') + }) }) }) }) @@ -297,6 +313,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`) + }) }) describe('delete self', () => { @@ -309,6 +329,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Moderator can not delete his own account!') + }) }) describe('delete with success', () => { @@ -338,6 +362,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith(`Could not find user with userId: ${user.id}`) + }) }) }) }) @@ -405,6 +433,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`) + }) }) describe('user to undelete is not deleted', () => { @@ -422,6 +454,10 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('User is not deleted') + }) + describe('undelete deleted user', () => { beforeAll(async () => { await mutate({ mutation: deleteUser, variables: { userId: user.id } }) @@ -909,6 +945,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Could not find user with email: bibi@bloxberg.de', + ) + }) }) describe('user to create for is deleted', () => { @@ -928,6 +970,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'This user was deleted. Cannot create a contribution.', + ) + }) }) describe('user to create for has email not confirmed', () => { @@ -947,6 +995,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Contribution could not be saved, Email is not activated', + ) + }) }) describe('valid user to create for', () => { @@ -967,6 +1021,13 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'No information for available creations with the given creationDate=', + new Date('not-valid').toString(), + ) + }) }) describe('date of creation is four months ago', () => { @@ -987,6 +1048,13 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'No information for available creations with the given creationDate=', + variables.creationDate, + ) + }) }) describe('date of creation is in the future', () => { @@ -1007,6 +1075,13 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'No information for available creations with the given creationDate=', + variables.creationDate, + ) + }) }) describe('amount of creation is too high', () => { @@ -1024,6 +1099,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'The amount (2000 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + ) + }) }) describe('creation is valid', () => { @@ -1065,6 +1146,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'The amount (1000 GDD) to be created exceeds the amount (800 GDD) still available for this month.', + ) + }) }) }) }) @@ -1143,6 +1230,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Could not find UserContact with email: bob@baumeister.de', + ) + }) }) describe('user for creation to update is deleted', () => { @@ -1164,6 +1257,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('User was deleted (stephen@hawking.uk)') + }) }) describe('creation does not exist', () => { @@ -1185,6 +1282,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('No contribution found to given id.') + }) }) describe('user email does not match creation user', () => { @@ -1210,6 +1311,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'user of the pending contribution and send user does not correspond', + ) + }) }) describe('creation update is not valid', () => { @@ -1235,6 +1342,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'The amount (1900 GDD) to be created exceeds the amount (500 GDD) still available for this month.', + ) + }) }) describe('creation update is successful changing month', () => { @@ -1371,6 +1484,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution not found for given id: -1') + }) }) describe('creation id does exist', () => { @@ -1407,6 +1524,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution not found for given id: -1') + }) }) describe('confirm own creation', () => { @@ -1434,6 +1555,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Moderator can not confirm own contribution') + }) }) describe('confirm creation for other user', () => { @@ -2041,6 +2166,12 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'Start-Date is not initialized. A Start-Date must be set!', + ) + }) + it('returns an error if missing endDate', async () => { await expect( mutate({ @@ -2057,6 +2188,12 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'End-Date is not initialized. An End-Date must be set!', + ) + }) + it('returns an error if endDate is before startDate', async () => { await expect( mutate({ @@ -2076,6 +2213,12 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `The value of validFrom must before or equals the validTo!`, + ) + }) + it('returns an error if name is an empty string', async () => { await expect( mutate({ @@ -2092,6 +2235,10 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('The name must be initialized!') + }) + it('returns an error if name is shorter than 5 characters', async () => { await expect( mutate({ @@ -2112,6 +2259,12 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`, + ) + }) + it('returns an error if name is longer than 100 characters', async () => { await expect( mutate({ @@ -2132,6 +2285,12 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`, + ) + }) + it('returns an error if memo is an empty string', async () => { await expect( mutate({ @@ -2148,6 +2307,10 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('The memo must be initialized!') + }) + it('returns an error if memo is shorter than 5 characters', async () => { await expect( mutate({ @@ -2168,6 +2331,12 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`, + ) + }) + it('returns an error if memo is longer than 255 characters', async () => { await expect( mutate({ @@ -2188,6 +2357,12 @@ describe('AdminResolver', () => { ) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`, + ) + }) + it('returns an error if amount is not positive', async () => { await expect( mutate({ @@ -2205,6 +2380,12 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + 'The amount=0 must be initialized with a positiv value!', + ) + }) }) describe('listContributionLinks', () => { @@ -2260,6 +2441,10 @@ describe('AdminResolver', () => { }) }) + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1') + }) + describe('valid id', () => { let linkId: number beforeAll(async () => { @@ -2325,6 +2510,10 @@ describe('AdminResolver', () => { }), ) }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1') + }) }) describe('valid id', () => { diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 4f1cec0e0..9987dfae6 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -21,7 +21,7 @@ export const validateContribution = ( if (index < 0) { logger.error( 'No information for available creations with the given creationDate=', - creationDate, + creationDate.toString(), ) throw new Error('No information for available creations for the given date') } From df2519784211723a52c9415770a0c6c66cc3a4d2 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 29 Sep 2022 11:39:01 +0200 Subject: [PATCH 07/21] solved unnecessary date creation regarding one invalid date testing --- backend/src/graphql/resolver/AdminResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 6576a7d04..53097abbe 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1025,7 +1025,7 @@ describe('AdminResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( 'No information for available creations with the given creationDate=', - new Date('not-valid').toString(), + 'Invalid Date', ) }) }) From 34b6e0d48f487294573c9c603e70225a09622f62 Mon Sep 17 00:00:00 2001 From: joseji Date: Fri, 7 Oct 2022 12:32:00 +0200 Subject: [PATCH 08/21] added new events --- backend/src/event/Event.ts | 68 +++++++++++++++++++++++++- backend/src/event/EventProtocolType.ts | 8 +++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index cec94e5bf..8523d13ce 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -74,6 +74,14 @@ export class EventContributionConfirm extends EventBasicCtX {} export class EventContributionDeny extends EventBasicCtX {} export class EventContributionLinkDefine extends EventBasicCt {} export class EventContributionLinkActivateRedeem extends EventBasicCt {} +export class EventDeleteUser extends EventBasicUserId {} +export class EventUndeleteUser extends EventBasicUserId {} +export class EventChangeUserRole extends EventBasicUserId {} +export class EventAdminUpdateContribution extends EventBasicCt {} +export class EventAdminDeleteContribution extends EventBasicCt {} +export class EventCreateContributionLink extends EventBasicCt {} +export class EventDeleteContributionLink extends EventBasicCt {} +export class EventUpdateContributionLink extends EventBasicCt {} export class Event { constructor() @@ -318,14 +326,14 @@ export class Event { } public setEventContributionConfirm(ev: EventContributionConfirm): Event { - this.setByBasicCtX(ev.userId, ev.xUserId, ev.xCommunityId, ev.contributionId, ev.amount) + this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId) this.type = EventProtocolType.CONTRIBUTION_CONFIRM return this } public setEventContributionDeny(ev: EventContributionDeny): Event { - this.setByBasicCtX(ev.userId, ev.xUserId, ev.xCommunityId, ev.contributionId, ev.amount) + this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId) this.type = EventProtocolType.CONTRIBUTION_DENY return this @@ -345,6 +353,62 @@ export class Event { return this } + public setEventDeleteUser(ev: EventDeleteUser): Event { + this.setByBasicUser(ev.userId) + this.type = EventProtocolType.DELETE_USER + + return this + } + + public setEventUndeleteUser(ev: EventUndeleteUser): Event { + this.setByBasicUser(ev.userId) + this.type = EventProtocolType.UNDELETE_USER + + return this + } + + public setEventChangeUserRole(ev: EventChangeUserRole): Event { + this.setByBasicUser(ev.userId) + this.type = EventProtocolType.CHANGE_USER_ROLE + + return this + } + + public setEventAdminUpdateContribution(ev: EventAdminUpdateContribution): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.ADMIN_UPDATE_CONTRIBUTION + + return this + } + + public setEventAdminDeleteContribution(ev: EventAdminDeleteContribution): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.ADMIN_DELETE_CONTRIBUTION + + return this + } + + public setEventCreateContributionLink(ev: EventCreateContributionLink): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.CREATE_CONTRIBUTION_LINK + + return this + } + + public setEventDeleteContributionLink(ev: EventDeleteContributionLink): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.DELETE_CONTRIBUTION_LINK + + return this + } + + public setEventUpdateContributionLink(ev: EventUpdateContributionLink): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.UPDATE_CONTRIBUTION_LINK + + return this + } + setByBasicUser(userId: number): Event { this.setEventBasic() this.userId = userId diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts index d53eb6961..a487fdd0b 100644 --- a/backend/src/event/EventProtocolType.ts +++ b/backend/src/event/EventProtocolType.ts @@ -35,4 +35,12 @@ export enum EventProtocolType { CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', + DELETE_USER = 'DELETE_USER', + UNDELETE_USER = 'UNDELETE_USER', + CHANGE_USER_ROLE = 'CHANGE_USER_ROLE', + ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION', + ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION', + CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', + DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', + UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', } From 088e30daae30c623e814cd08e5133077b771e678 Mon Sep 17 00:00:00 2001 From: joseji Date: Fri, 7 Oct 2022 12:36:31 +0200 Subject: [PATCH 09/21] added admin events for contributions --- backend/src/event/Event.ts | 24 ++++++++++++++++++++++++ backend/src/event/EventProtocolType.ts | 3 +++ 2 files changed, 27 insertions(+) diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 8523d13ce..09a31d4e0 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -66,6 +66,9 @@ export class EventTransactionCreation extends EventBasicTx {} export class EventTransactionReceive extends EventBasicTxX {} export class EventTransactionReceiveRedeem extends EventBasicTxX {} export class EventContributionCreate extends EventBasicCt {} +export class EventAdminContributionCreate extends EventBasicCt {} +export class EventAdminContributionDelete extends EventBasicCt {} +export class EventAdminContributionUpdate extends EventBasicCt {} export class EventUserCreateContributionMessage extends EventBasicCtMsg {} export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} export class EventContributionDelete extends EventBasicCt {} @@ -297,6 +300,27 @@ export class Event { return this } + public setEventAdminContributionCreate(ev: EventAdminContributionCreate): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.ADMIN_CONTRIBUTION_CREATE + + return this + } + + public setEventAdminContributionDelete(ev: EventAdminContributionDelete): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.ADMIN_CONTRIBUTION_DELETE + + return this + } + + public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE + + return this + } + public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event { this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId) this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts index a487fdd0b..b7c2f0151 100644 --- a/backend/src/event/EventProtocolType.ts +++ b/backend/src/event/EventProtocolType.ts @@ -33,6 +33,9 @@ export enum EventProtocolType { CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM', CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE', CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', + ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE', + ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE', + ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE', USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', DELETE_USER = 'DELETE_USER', From a50871185f19909e7f7019621edd2c253ba4e030 Mon Sep 17 00:00:00 2001 From: joseji Date: Fri, 7 Oct 2022 12:47:07 +0200 Subject: [PATCH 10/21] new contribution related admin events implemented and working --- .../graphql/resolver/AdminResolver.test.ts | 31 ++++++++++++++-- backend/src/graphql/resolver/AdminResolver.ts | 35 +++++++++++++++---- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 53097abbe..ad847bde1 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1121,10 +1121,10 @@ describe('AdminResolver', () => { ) }) - it('stores the create contribution event in the database', async () => { + it('stores the admin create contribution event in the database', async () => { await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ - type: EventProtocolType.CONTRIBUTION_CREATE, + type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE, userId: admin.id, }), ) @@ -1376,6 +1376,15 @@ describe('AdminResolver', () => { }), ) }) + + it('stores the admin update contribution event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, + userId: admin.id, + }), + ) + }) }) describe('creation update is successful without changing month', () => { @@ -1404,6 +1413,15 @@ describe('AdminResolver', () => { }), ) }) + + it('stores the admin update contribution event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, + userId: admin.id, + }), + ) + }) }) }) @@ -1505,6 +1523,15 @@ describe('AdminResolver', () => { }), ) }) + + it('stores the admin delete contribution event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, + userId: admin.id, + }), + ) + }) }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index b03da9dc4..f228c01e3 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -69,8 +69,10 @@ import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributio import { eventProtocol } from '@/event/EventProtocolEmitter' import { Event, + EventAdminContributionCreate, + EventAdminContributionDelete, + EventAdminContributionUpdate, EventContributionConfirm, - EventContributionCreate, EventSendConfirmationEmail, } from '@/event/Event' @@ -278,11 +280,13 @@ export class AdminResolver { logger.trace('contribution to save', contribution) await Contribution.save(contribution) - const eventCreateContribution = new EventContributionCreate() - eventCreateContribution.userId = moderator.id - eventCreateContribution.amount = amount - eventCreateContribution.contributionId = contribution.id - await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution)) + const eventAdminCreateContribution = new EventAdminContributionCreate() + eventAdminCreateContribution.userId = moderator.id + eventAdminCreateContribution.amount = amount + eventAdminCreateContribution.contributionId = contribution.id + await eventProtocol.writeEvent( + event.setEventAdminContributionCreate(eventAdminCreateContribution), + ) return getUserCreation(emailContact.userId) } @@ -382,6 +386,15 @@ export class AdminResolver { result.creation = await getUserCreation(user.id) + const event = new Event() + const eventAdminContributionUpdate = new EventAdminContributionUpdate() + eventAdminContributionUpdate.userId = user.id + eventAdminContributionUpdate.amount = amount + eventAdminContributionUpdate.contributionId = contributionToUpdate.id + await eventProtocol.writeEvent( + event.setEventAdminContributionUpdate(eventAdminContributionUpdate), + ) + return result } @@ -431,6 +444,16 @@ export class AdminResolver { contribution.contributionStatus = ContributionStatus.DELETED await contribution.save() const res = await contribution.softRemove() + + const event = new Event() + const eventAdminContributionDelete = new EventAdminContributionDelete() + eventAdminContributionDelete.userId = contribution.userId + eventAdminContributionDelete.amount = contribution.amount + eventAdminContributionDelete.contributionId = contribution.id + await eventProtocol.writeEvent( + event.setEventAdminContributionDelete(eventAdminContributionDelete), + ) + return !!res } From 3bd339d798d246400361898183a1baa8702c4334 Mon Sep 17 00:00:00 2001 From: joseji Date: Mon, 24 Oct 2022 22:01:19 +0200 Subject: [PATCH 11/21] fixed wrong removals of negative numbers --- backend/src/graphql/resolver/TransactionResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index afe8a7974..4bdd343b2 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -83,7 +83,7 @@ export const executeTransaction = async ( transactionSend.memo = memo transactionSend.userId = sender.id transactionSend.linkedUserId = recipient.id - transactionSend.amount = amount + transactionSend.amount = amount.mul(-1) transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate transactionSend.decay = sendBalance.decay.decay @@ -151,7 +151,7 @@ export const executeTransaction = async ( eventTransactionSend.userId = transactionSend.userId eventTransactionSend.xUserId = transactionSend.linkedUserId eventTransactionSend.transactionId = transactionSend.id - eventTransactionSend.amount = transactionSend.amount + eventTransactionSend.amount = transactionSend.amount.mul(-1) await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) const eventTransactionReceive = new EventTransactionReceive() From 7662d281f73174698cddd1829cdf98e441e65e2c Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 25 Oct 2022 12:37:08 +0200 Subject: [PATCH 12/21] fixed test with new info --- .../graphql/resolver/AdminResolver.test.ts | 38 +++++++++++-------- backend/src/graphql/resolver/AdminResolver.ts | 2 +- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 9100d02b6..5806884b6 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1300,7 +1300,9 @@ describe('AdminResolver', () => { email: 'bibi@bloxberg.de', amount: new Decimal(300), memo: 'Danke Bibi!', - creationDate: new Date().toString(), + creationDate: creation + ? creation.contributionDate.toString() + : new Date().toString(), }, }), ).resolves.toEqual( @@ -1323,7 +1325,7 @@ describe('AdminResolver', () => { describe('creation update is not valid', () => { // as this test has not clearly defined that date, it is a false positive - it.skip('throws an error', async () => { + it('throws an error', async () => { await expect( mutate({ mutation: adminUpdateContribution, @@ -1332,14 +1334,16 @@ describe('AdminResolver', () => { email: 'peter@lustig.de', amount: new Decimal(1900), memo: 'Danke Peter!', - creationDate: new Date().toString(), + creationDate: creation + ? creation.contributionDate.toString() + : new Date().toString(), }, }), ).resolves.toEqual( expect.objectContaining({ errors: [ new GraphQLError( - 'The amount (1900 GDD) to be created exceeds the amount (500 GDD) still available for this month.', + 'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', ), ], }), @@ -1348,14 +1352,14 @@ describe('AdminResolver', () => { it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - 'The amount (1900 GDD) to be created exceeds the amount (500 GDD) still available for this month.', + 'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', ) }) }) describe('creation update is successful changing month', () => { // skipped as changing the month is currently disable - it.skip('returns update creation object', async () => { + it('returns update creation object', async () => { await expect( mutate({ mutation: adminUpdateContribution, @@ -1364,7 +1368,9 @@ describe('AdminResolver', () => { email: 'peter@lustig.de', amount: new Decimal(300), memo: 'Danke Peter!', - creationDate: new Date().toString(), + creationDate: creation + ? creation.contributionDate.toString() + : new Date().toString(), }, }), ).resolves.toEqual( @@ -1374,7 +1380,7 @@ describe('AdminResolver', () => { date: expect.any(String), memo: 'Danke Peter!', amount: '300', - creation: ['1000', '1000', '200'], + creation: ['1000', '700', '500'], }, }, }), @@ -1393,7 +1399,7 @@ describe('AdminResolver', () => { describe('creation update is successful without changing month', () => { // actually this mutation IS changing the month - it.skip('returns update creation object', async () => { + it('returns update creation object', async () => { await expect( mutate({ mutation: adminUpdateContribution, @@ -1402,7 +1408,9 @@ describe('AdminResolver', () => { email: 'peter@lustig.de', amount: new Decimal(200), memo: 'Das war leider zu Viel!', - creationDate: new Date().toString(), + creationDate: creation + ? creation.contributionDate.toString() + : new Date().toString(), }, }), ).resolves.toEqual( @@ -1412,7 +1420,7 @@ describe('AdminResolver', () => { date: expect.any(String), memo: 'Das war leider zu Viel!', amount: '200', - creation: ['1000', '1000', '300'], + creation: ['1000', '800', '500'], }, }, }), @@ -1446,10 +1454,10 @@ describe('AdminResolver', () => { lastName: 'Lustig', email: 'peter@lustig.de', date: expect.any(String), - memo: 'Herzlich Willkommen bei Gradido!', - amount: '400', + memo: 'Das war leider zu Viel!', + amount: '200', moderator: admin.id, - creation: ['1000', '600', '500'], + creation: ['1000', '800', '500'], }, { id: expect.any(Number), @@ -1460,7 +1468,7 @@ describe('AdminResolver', () => { memo: 'Grundeinkommen', amount: '500', moderator: admin.id, - creation: ['1000', '600', '500'], + creation: ['1000', '800', '500'], }, { id: expect.any(Number), diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 634779bf6..aab84e911 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -348,7 +348,6 @@ export class AdminResolver { const contributionToUpdate = await DbContribution.findOne({ where: { id, confirmedAt: IsNull() }, }) - if (!contributionToUpdate) { logger.error('No contribution found to given id.') throw new Error('No contribution found to given id.') @@ -366,6 +365,7 @@ export class AdminResolver { const creationDateObj = new Date(creationDate) let creations = await getUserCreation(user.id) + if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { creations = updateCreations(creations, contributionToUpdate) } else { From b5edc5f4760d4860e612774090d8da357b3a1483 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 25 Oct 2022 13:45:43 +0200 Subject: [PATCH 13/21] clean up --- .../src/graphql/resolver/util/creations.ts | 46 ++----------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index d54ee2ebf..8371f96f1 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -49,16 +49,6 @@ export const getUserCreations = async ( const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day' logger.trace('getUserCreations dateFilter=', dateFilter) - /** - SELECT MONTH(contribution_date) as month, user_id, created_at, sum(amount), confirmed_at, deleted_at - FROM `contributions` - where user_id = 776 - and contribution_date >= last_day(curdate() - interval 3 month) + interval 1 day - and deleted_at IS NULL - and denied_at IS NULL - if(!includePending) and confirmed_at IS NOT NULL - group by month, user_id; - */ const sumAmountContributionPerUserAndLast3MonthQuery = queryRunner.manager .createQueryBuilder(Contribution, 'c') .select('month(contribution_date)', 'month') @@ -70,35 +60,15 @@ export const getUserCreations = async ( .andWhere('denied_at IS NULL') .groupBy('month') .addGroupBy('userId') + if (!includePending) { sumAmountContributionPerUserAndLast3MonthQuery.andWhere('confirmed_at IS NOT NULL') } + const sumAmountContributionPerUserAndLast3Month = await sumAmountContributionPerUserAndLast3MonthQuery.getRawMany() - /* - const unionString = includePending - ? ` - UNION - SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions - WHERE user_id IN (${ids.toString()}) - AND contribution_date >= ${dateFilter} - AND confirmed_at IS NULL AND deleted_at IS NULL` - : '' - logger.trace('getUserCreations unionString=', unionString) - - const unionQuery = await queryRunner.manager.query(` - SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM - (SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions - WHERE user_id IN (${ids.toString()}) - AND type_id = ${TransactionTypeId.CREATION} - AND creation_date >= ${dateFilter} - ${unionString}) AS result - GROUP BY month, userId - ORDER BY date DESC - `) - logger.trace('getUserCreations unionQuery=', unionQuery) - */ + logger.trace(sumAmountContributionPerUserAndLast3Month) await queryRunner.release() @@ -113,16 +83,6 @@ export const getUserCreations = async ( return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) }), } - // const creations = months.map((month) => { - // const creation = openCreation.find( - // (raw: { month: string; userId: string; creation: number[] }) => - // parseInt(raw.month) === month && parseInt(raw.userId) === id, - // ) - // return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) - // }) - // // eslint-disable-next-line no-console - // console.log('id: ', id, 'creations: ', creations.toString()) - // return { id, creations } }) } From b2a1d7d67234380472425832ffd47a918853f4e1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 26 Oct 2022 06:38:49 +0200 Subject: [PATCH 14/21] include pending contributions for validation of contribution link, test it --- .../resolver/TransactionLinkResolver.test.ts | 151 ++++++++++++------ .../resolver/TransactionLinkResolver.ts | 2 +- 2 files changed, 107 insertions(+), 46 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 3d40adbf6..275242bd3 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -6,8 +6,15 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { cleanDB, testEnvironment } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' -import { login, createContributionLink, redeemTransactionLink } from '@/seeds/graphql/mutations' +import { + login, + createContributionLink, + redeemTransactionLink, + createContribution, + updateContribution, +} from '@/seeds/graphql/mutations' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' +import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import Decimal from 'decimal.js-light' import { GraphQLError } from 'graphql' @@ -32,6 +39,7 @@ describe('TransactionLinkResolver', () => { describe('redeem daily Contribution Link', () => { const now = new Date() let contributionLink: DbContributionLink | undefined + let contribution: UnconfirmedContribution | undefined beforeAll(async () => { await mutate({ @@ -79,56 +87,59 @@ describe('TransactionLinkResolver', () => { ) }) - it('allows the user to redeem the contribution link', async () => { - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + (contributionLink ? contributionLink.code : ''), - }, - }), - ).resolves.toMatchObject({ - data: { - redeemTransactionLink: true, - }, - errors: undefined, - }) - }) - - it('does not allow the user to redeem the contribution link a second time on the same day', async () => { - await expect( - mutate({ - mutation: redeemTransactionLink, - variables: { - code: 'CL-' + (contributionLink ? contributionLink.code : ''), - }, - }), - ).resolves.toMatchObject({ - errors: [ - new GraphQLError( - 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', - ), - ], - }) - }) - - describe('after one day', () => { + describe('user has pending contribution of 1000 GDD', () => { beforeAll(async () => { - jest.useFakeTimers() - /* eslint-disable-next-line @typescript-eslint/no-empty-function */ - setTimeout(() => {}, 1000 * 60 * 60 * 24) - jest.runAllTimers() await mutate({ mutation: login, - variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + const result = await mutate({ + mutation: createContribution, + variables: { + amount: new Decimal(1000), + memo: 'I was brewing potions for the community the whole month', + creationDate: now.toISOString(), + }, + }) + contribution = result.data.createContribution + }) + + it('does not allow the user to redeem the contribution link', async () => { + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).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.', + ), + ], + }) + }) + }) + + describe('user has no pending contributions that would not allow to redeem the link', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + await mutate({ + mutation: updateContribution, + variables: { + contributionId: contribution ? contribution.id : -1, + amount: new Decimal(800), + memo: 'I was brewing potions for the community the whole month', + creationDate: now.toISOString(), + }, }) }) - afterAll(() => { - jest.useRealTimers() - }) - - it('allows the user to redeem the contribution link again', async () => { + it('allows the user to redeem the contribution link', async () => { await expect( mutate({ mutation: redeemTransactionLink, @@ -160,6 +171,56 @@ describe('TransactionLinkResolver', () => { ], }) }) + + describe('after one day', () => { + beforeAll(async () => { + jest.useFakeTimers() + /* eslint-disable-next-line @typescript-eslint/no-empty-function */ + setTimeout(() => {}, 1000 * 60 * 60 * 24) + jest.runAllTimers() + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + jest.useRealTimers() + }) + + it('allows the user to redeem the contribution link again', async () => { + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + data: { + redeemTransactionLink: true, + }, + errors: undefined, + }) + }) + + it('does not allow the user to redeem the contribution link a second time on the same day', async () => { + await expect( + mutate({ + mutation: redeemTransactionLink, + variables: { + code: 'CL-' + (contributionLink ? contributionLink.code : ''), + }, + }), + ).resolves.toMatchObject({ + errors: [ + new GraphQLError( + 'Creation from contribution link was not successful. Error: Contribution link already redeemed today', + ), + ], + }) + }) + }) }) }) }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 4ba5dcd0b..74c531c54 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -258,7 +258,7 @@ export class TransactionLinkResolver { } } - const creations = await getUserCreation(user.id, false) + const creations = await getUserCreation(user.id) logger.info('open creations', creations) validateContribution(creations, contributionLink.amount, now) const contribution = new DbContribution() From 1407ea0e8b3896f19f9ed534f0e1b6a83f07e829 Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 26 Oct 2022 11:24:04 +0200 Subject: [PATCH 15/21] skipped test for changing month --- backend/src/graphql/resolver/AdminResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 5806884b6..b5711cd57 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1357,7 +1357,7 @@ describe('AdminResolver', () => { }) }) - describe('creation update is successful changing month', () => { + describe.skip('creation update is successful changing month', () => { // skipped as changing the month is currently disable it('returns update creation object', async () => { await expect( From c3b624f36fcd0207730719628ed34dc48461323e Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 27 Oct 2022 19:55:23 +0200 Subject: [PATCH 16/21] add order by clause --- backend/src/graphql/resolver/util/creations.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 8371f96f1..abf4017cb 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -60,6 +60,7 @@ export const getUserCreations = async ( .andWhere('denied_at IS NULL') .groupBy('month') .addGroupBy('userId') + .orderBy('month', 'DESC') if (!includePending) { sumAmountContributionPerUserAndLast3MonthQuery.andWhere('confirmed_at IS NOT NULL') From 4327c600f92a41ed3160eb397ff750a938d3e832 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 28 Oct 2022 08:59:13 +0200 Subject: [PATCH 17/21] fix: Link in Contribution Messages --- frontend/src/components/ContributionMessages/LinkifyMessage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ContributionMessages/LinkifyMessage.vue b/frontend/src/components/ContributionMessages/LinkifyMessage.vue index 5d6ec34cb..fc577adf9 100644 --- a/frontend/src/components/ContributionMessages/LinkifyMessage.vue +++ b/frontend/src/components/ContributionMessages/LinkifyMessage.vue @@ -1,7 +1,7 @@