From b95a8081fc6704deb461f0227112e8ad18c02fd4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Jun 2022 12:17:28 +0200 Subject: [PATCH 01/23] Refactored getUserCreation & isContributionValid method to own files. --- backend/src/graphql/resolver/AdminResolver.ts | 35 +++---------------- .../graphql/resolver/util/getUserCreation.ts | 9 +++++ .../resolver/util/isContributionValid.ts | 24 +++++++++++++ 3 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 backend/src/graphql/resolver/util/getUserCreation.ts create mode 100644 backend/src/graphql/resolver/util/isContributionValid.ts diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 825afae94..8147c9c4b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -46,11 +46,13 @@ import { checkOptInCode, activationLink, printTimeDuration } from './UserResolve import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' +import { getUserCreation } from './util/getUserCreation' +import { isContributionValid } from './util/isContributionValid' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? const MAX_CREATION_AMOUNT = new Decimal(1000) -const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT] +export const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT] @Resolver() export class AdminResolver { @@ -650,13 +652,7 @@ interface CreationMap { creations: Decimal[] } -export const getUserCreation = async (id: number, includePending = true): Promise => { - logger.trace('getUserCreation', id, includePending) - const creations = await getUserCreations([id], includePending) - return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE -} - -async function getUserCreations(ids: number[], includePending = true): Promise { +export async function getUserCreations(ids: number[], includePending = true): Promise { logger.trace('getUserCreations:', ids, includePending) const months = getCreationMonths() logger.trace('getUserCreations months', months) @@ -713,27 +709,6 @@ function updateCreations(creations: Decimal[], contribution: Contribution): Deci return creations } -export const isContributionValid = ( - creations: Decimal[], - amount: Decimal, - creationDate: Date, -): boolean => { - logger.trace('isContributionValid', creations, amount, creationDate) - const index = getCreationIndex(creationDate.getMonth()) - - if (index < 0) { - throw new Error('No information for available creations for the given date') - } - - if (amount.greaterThan(creations[index].toString())) { - throw new Error( - `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`, - ) - } - - return true -} - const getCreationMonths = (): number[] => { const now = new Date(Date.now()) return [ @@ -743,6 +718,6 @@ const getCreationMonths = (): number[] => { ].reverse() } -const getCreationIndex = (month: number): number => { +export const getCreationIndex = (month: number): number => { return getCreationMonths().findIndex((el) => el === month + 1) } diff --git a/backend/src/graphql/resolver/util/getUserCreation.ts b/backend/src/graphql/resolver/util/getUserCreation.ts new file mode 100644 index 000000000..445e25b9c --- /dev/null +++ b/backend/src/graphql/resolver/util/getUserCreation.ts @@ -0,0 +1,9 @@ +import { backendLogger as logger } from '@/server/logger' +import Decimal from 'decimal.js-light' +import { getUserCreations, FULL_CREATION_AVAILABLE } from '../AdminResolver' + +export const getUserCreation = async (id: number, includePending = true): Promise => { + logger.trace('getUserCreation', id, includePending) + const creations = await getUserCreations([id], includePending) + return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE +} diff --git a/backend/src/graphql/resolver/util/isContributionValid.ts b/backend/src/graphql/resolver/util/isContributionValid.ts new file mode 100644 index 000000000..5ae7755c1 --- /dev/null +++ b/backend/src/graphql/resolver/util/isContributionValid.ts @@ -0,0 +1,24 @@ +import { backendLogger as logger } from '@/server/logger' +import Decimal from 'decimal.js-light' +import { getCreationIndex } from '../AdminResolver' + +export const isContributionValid = ( + creations: Decimal[], + amount: Decimal, + creationDate: Date, +): boolean => { + logger.trace('isContributionValid', creations, amount, creationDate) + const index = getCreationIndex(creationDate.getMonth()) + + if (index < 0) { + throw new Error('No information for available creations for the given date') + } + + if (amount.greaterThan(creations[index].toString())) { + throw new Error( + `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`, + ) + } + + return true +} From 5ebd0993d9e2c793119280dd54ff922ebbdb90b0 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Jun 2022 12:18:18 +0200 Subject: [PATCH 02/23] New Args definition for CreateContribution. --- backend/src/graphql/arg/CreateContributionArgs.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/src/graphql/arg/CreateContributionArgs.ts diff --git a/backend/src/graphql/arg/CreateContributionArgs.ts b/backend/src/graphql/arg/CreateContributionArgs.ts new file mode 100644 index 000000000..88ec89eac --- /dev/null +++ b/backend/src/graphql/arg/CreateContributionArgs.ts @@ -0,0 +1,15 @@ +import { ArgsType, Field, InputType } from 'type-graphql' +import Decimal from 'decimal.js-light' + +@InputType() +@ArgsType() +export default class CreateContributionArgs { + @Field(() => Decimal) + amount: Decimal + + @Field(() => String) + memo: string + + @Field(() => String) + creationDate: string +} From 2d90fdd64e66ea6dcaf2b76c04c7d9fe284a378b Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Jun 2022 12:19:02 +0200 Subject: [PATCH 03/23] New Resolver for Contribution, createContribution method. --- .../graphql/resolver/ContributionResolver.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 backend/src/graphql/resolver/ContributionResolver.ts diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts new file mode 100644 index 000000000..728ff6a49 --- /dev/null +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -0,0 +1,45 @@ +import { Context, getUser } from '@/server/context' +import { backendLogger as logger } from '@/server/logger' +import { Contribution } from '@entity/Contribution' +import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' +import CreateContributionArgs from '../arg/CreateContributionArgs' +import { getUserCreation } from './util/getUserCreation' +import { isContributionValid } from './util/isContributionValid' + +@Resolver() +export class ContributionResolver { + @Authorized([]) + @Mutation(() => Contribution) + async createContribution( + @Args() { amount, memo, creationDate }: CreateContributionArgs, + @Ctx() context: Context, + ): Promise { + logger.trace('createContribution..') + const user = getUser(context) + if (!user) { + throw new Error(`Could not find user`) + } + if (user.deletedAt) { + throw new Error('This user was deleted. Cannot create a contribution.') + } + if (!user.emailChecked) { + throw new Error('Contribution could not be saved, Email is not activated') + } + const creations = await getUserCreation(user.id) + logger.trace('creations', creations) + const creationDateObj = new Date(creationDate) + if (!isContributionValid(creations, amount, creationDateObj)) { + throw new Error('Contribution is not valid') + } + const contribution = Contribution.create() + contribution.userId = user.id + contribution.amount = amount + contribution.createdAt = new Date() + contribution.contributionDate = creationDateObj + contribution.memo = memo + + logger.trace('contribution to save', contribution) + await Contribution.save(contribution) + return contribution + } +} From d062a10a34f72088785428b8698dd82a6bcbb3b8 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 27 Jun 2022 13:37:03 +0200 Subject: [PATCH 04/23] Fix linting. --- backend/src/graphql/resolver/AdminResolver.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 8147c9c4b..a0ebb06d4 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -52,7 +52,11 @@ import { isContributionValid } from './util/isContributionValid' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? const MAX_CREATION_AMOUNT = new Decimal(1000) -export const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT] +export const FULL_CREATION_AVAILABLE = [ + MAX_CREATION_AMOUNT, + MAX_CREATION_AMOUNT, + MAX_CREATION_AMOUNT, +] @Resolver() export class AdminResolver { @@ -652,7 +656,10 @@ interface CreationMap { creations: Decimal[] } -export async function getUserCreations(ids: number[], includePending = true): Promise { +export async function getUserCreations( + ids: number[], + includePending = true, +): Promise { logger.trace('getUserCreations:', ids, includePending) const months = getCreationMonths() logger.trace('getUserCreations months', months) From 6ac0106470ce70ce9f80797a036a3ab4a690e917 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 27 Jun 2022 13:37:47 +0200 Subject: [PATCH 05/23] Change import for refactored methods. --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index c607247b9..83fd6367f 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -28,7 +28,8 @@ import { executeTransaction } from './TransactionResolver' import { Order } from '@enum/Order' import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { getUserCreation, isContributionValid } from './AdminResolver' +import { isContributionValid } from './util/isContributionValid' +import { getUserCreation } from './util/getUserCreation' import { Decay } from '@model/Decay' import Decimal from 'decimal.js-light' import { TransactionTypeId } from '@enum/TransactionTypeId' From c03509c5258d2d163e04a106619d1cd02e0af271 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 27 Jun 2022 13:40:36 +0200 Subject: [PATCH 06/23] Change response value to boolean, add RIGHTS for createContribution, add new RIGHT in ROLES. --- backend/src/auth/RIGHTS.ts | 1 + backend/src/auth/ROLES.ts | 1 + backend/src/graphql/resolver/ContributionResolver.ts | 10 +++++----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index fc2b5342c..c10fc96de 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -25,6 +25,7 @@ export enum RIGHTS { REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK', LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS', GDT_BALANCE = 'GDT_BALANCE', + CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION', // Admin SEARCH_USERS = 'SEARCH_USERS', SET_USER_ROLE = 'SET_USER_ROLE', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 891fe1844..2d9ac2deb 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -23,6 +23,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.REDEEM_TRANSACTION_LINK, RIGHTS.LIST_TRANSACTION_LINKS, RIGHTS.GDT_BALANCE, + RIGHTS.CREATE_CONTRIBUTION, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 728ff6a49..0f516dc7c 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -1,3 +1,4 @@ +import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Contribution } from '@entity/Contribution' @@ -8,13 +9,12 @@ import { isContributionValid } from './util/isContributionValid' @Resolver() export class ContributionResolver { - @Authorized([]) - @Mutation(() => Contribution) + @Authorized([RIGHTS.CREATE_CONTRIBUTION]) + @Mutation(() => Boolean) async createContribution( @Args() { amount, memo, creationDate }: CreateContributionArgs, @Ctx() context: Context, - ): Promise { - logger.trace('createContribution..') + ): Promise { const user = getUser(context) if (!user) { throw new Error(`Could not find user`) @@ -40,6 +40,6 @@ export class ContributionResolver { logger.trace('contribution to save', contribution) await Contribution.save(contribution) - return contribution + return true } } From 4d4f6979dafee4a390f118817b86bab3730b093f Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 27 Jun 2022 13:41:11 +0200 Subject: [PATCH 07/23] Change moderator so that it can be null. --- backend/src/graphql/model/UnconfirmedContribution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/model/UnconfirmedContribution.ts b/backend/src/graphql/model/UnconfirmedContribution.ts index 69001c19b..db4c08ce4 100644 --- a/backend/src/graphql/model/UnconfirmedContribution.ts +++ b/backend/src/graphql/model/UnconfirmedContribution.ts @@ -27,8 +27,8 @@ export class UnconfirmedContribution { @Field(() => Decimal) amount: Decimal - @Field(() => Number) - moderator: number + @Field(() => Number, { nullable: true }) + moderator: number | null @Field(() => [Decimal]) creation: Decimal[] From 38e04b49799a5f16dd8dfa078e57b40a8a48ef08 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 27 Jun 2022 17:12:59 +0200 Subject: [PATCH 08/23] Remove checks on users since the authorization token handels it. --- backend/src/graphql/resolver/ContributionResolver.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 0f516dc7c..f02e52ad8 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -16,15 +16,6 @@ export class ContributionResolver { @Ctx() context: Context, ): Promise { const user = getUser(context) - if (!user) { - throw new Error(`Could not find user`) - } - if (user.deletedAt) { - throw new Error('This user was deleted. Cannot create a contribution.') - } - if (!user.emailChecked) { - throw new Error('Contribution could not be saved, Email is not activated') - } const creations = await getUserCreation(user.id) logger.trace('creations', creations) const creationDateObj = new Date(creationDate) From a56908b8f5eb33cdcb0ecf334f3553ff4b723d5a Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 27 Jun 2022 17:13:30 +0200 Subject: [PATCH 09/23] Adding some test for the ContributionToken. --- .../resolver/ContributionResolver.test.ts | 183 ++++++++++++++++++ backend/src/seeds/graphql/mutations.ts | 6 + 2 files changed, 189 insertions(+) create mode 100644 backend/src/graphql/resolver/ContributionResolver.test.ts diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts new file mode 100644 index 000000000..3e5407811 --- /dev/null +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -0,0 +1,183 @@ +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { peterLustig } from '@/seeds/users/peter-lustig' +import { createContribution } from '@/seeds/graphql/mutations' +import { login } from '@/seeds/graphql/queries' +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { GraphQLError } from 'graphql' +import { User } from '@entity/User' +import { Contribution } from '@entity/Contribution' +import { userFactory } from '@/seeds/factory/user' +import { garrickOllivander } from '@/seeds/users/garrick-ollivander' +import { stephenHawking } from '@/seeds/users/stephen-hawking' + +let mutate: any, query: any, con: any +let testEnv: any + +beforeAll(async () => { + testEnv = await testEnvironment() + mutate = testEnv.mutate + query = testEnv.query + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +let user: User +let creation: Contribution | void + +describe('ContributionResolver', () => { + describe('createContribution', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: createContribution, + variables: { amount: 100.0, memo: 'Test Contribution', creationDate: 'not-valid' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated with deleted user', () => { + beforeAll(async () => { + user = await userFactory(testEnv, stephenHawking) + await query({ + query: login, + variables: { email: 'stephen@hawking.uk', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + it('throws unauthorized error', async () => { + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated with user that has not checked his email', () => { + beforeAll(async () => { + user = await userFactory(testEnv, garrickOllivander) + await query({ + query: login, + variables: { email: 'garrick@ollivander.com', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + it('throws unauthorized error', async () => { + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated with valid user', () => { + beforeAll(async () => { + user = await userFactory(testEnv, bibiBloxberg) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + it('throws error when creationDate not-valid', async () => { + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No information for available creations for the given date')], + }), + ) + }) + + it('throws error when creationDate 3 month behind', async () => { + const date = new Date() + date.setMonth(date.getMonth() - 3) + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: date.toString(), + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No information for available creations for the given date')], + }), + ) + }) + + it('creates contribution', async () => { + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + createContribution: true, + }, + }), + ) + }) + }) + }) +}) diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index f2edf0821..5383398b1 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -230,3 +230,9 @@ export const deleteContributionLink = gql` deleteContributionLink(id: $id) } ` + +export const createContribution = gql` + mutation ($amount: Decimal!, $memo: String!, $creationDate: String!) { + createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) + } +` From 457016a7a75f20cd71daa11fd55051749d1f3923 Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 27 Jun 2022 17:43:10 +0200 Subject: [PATCH 10/23] Withdrew if contribution not valid statement since the method throws an exception if not valid. --- backend/src/graphql/resolver/ContributionResolver.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index f02e52ad8..0b779e263 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -2,8 +2,9 @@ import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Contribution } from '@entity/Contribution' -import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' +import { Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql' import CreateContributionArgs from '../arg/CreateContributionArgs' +import Paginated from '../arg/Paginated' import { getUserCreation } from './util/getUserCreation' import { isContributionValid } from './util/isContributionValid' @@ -19,9 +20,8 @@ export class ContributionResolver { const creations = await getUserCreation(user.id) logger.trace('creations', creations) const creationDateObj = new Date(creationDate) - if (!isContributionValid(creations, amount, creationDateObj)) { - throw new Error('Contribution is not valid') - } + isContributionValid(creations, amount, creationDateObj) + const contribution = Contribution.create() contribution.userId = user.id contribution.amount = amount From 5a51207895482b1b6559128dd005efd72e3e6970 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 28 Jun 2022 09:59:25 +0200 Subject: [PATCH 11/23] Remove unused imports. --- backend/src/graphql/resolver/ContributionResolver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 0b779e263..6f1ed3074 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -2,9 +2,8 @@ import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Contribution } from '@entity/Contribution' -import { Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql' +import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' import CreateContributionArgs from '../arg/CreateContributionArgs' -import Paginated from '../arg/Paginated' import { getUserCreation } from './util/getUserCreation' import { isContributionValid } from './util/isContributionValid' From 237c01af1b26b543f0e7f60f63b3ddf49db84da9 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 10:13:08 +0200 Subject: [PATCH 12/23] Refactor CreateContributionArgs to ContributionArgs so that it can be used for updateContribution. --- .../arg/{CreateContributionArgs.ts => ContributionArgs.ts} | 2 +- backend/src/graphql/resolver/ContributionResolver.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename backend/src/graphql/arg/{CreateContributionArgs.ts => ContributionArgs.ts} (84%) diff --git a/backend/src/graphql/arg/CreateContributionArgs.ts b/backend/src/graphql/arg/ContributionArgs.ts similarity index 84% rename from backend/src/graphql/arg/CreateContributionArgs.ts rename to backend/src/graphql/arg/ContributionArgs.ts index 88ec89eac..2fa1c5ced 100644 --- a/backend/src/graphql/arg/CreateContributionArgs.ts +++ b/backend/src/graphql/arg/ContributionArgs.ts @@ -3,7 +3,7 @@ import Decimal from 'decimal.js-light' @InputType() @ArgsType() -export default class CreateContributionArgs { +export default class ContributionArgs { @Field(() => Decimal) amount: Decimal diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 6f1ed3074..b083305e9 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -3,7 +3,7 @@ import { Context, getUser } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Contribution } from '@entity/Contribution' import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' -import CreateContributionArgs from '../arg/CreateContributionArgs' +import ContributionArgs from '../arg/ContributionArgs' import { getUserCreation } from './util/getUserCreation' import { isContributionValid } from './util/isContributionValid' @@ -12,7 +12,7 @@ export class ContributionResolver { @Authorized([RIGHTS.CREATE_CONTRIBUTION]) @Mutation(() => Boolean) async createContribution( - @Args() { amount, memo, creationDate }: CreateContributionArgs, + @Args() { amount, memo, creationDate }: ContributionArgs, @Ctx() context: Context, ): Promise { const user = getUser(context) From 6e5540b863fbf1edc02205a6e59245c867630b1b Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 10:49:53 +0200 Subject: [PATCH 13/23] Move all util methods to isContributionValid file, add const file for Resolvers. --- backend/src/graphql/resolver/AdminResolver.ts | 140 ++++-------------- .../graphql/resolver/ContributionResolver.ts | 3 +- backend/src/graphql/resolver/const/const.ts | 12 ++ .../graphql/resolver/util/getUserCreation.ts | 9 -- .../resolver/util/isContributionValid.ts | 103 ++++++++++++- 5 files changed, 137 insertions(+), 130 deletions(-) create mode 100644 backend/src/graphql/resolver/const/const.ts delete mode 100644 backend/src/graphql/resolver/util/getUserCreation.ts diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 841303bf6..5476fd8a1 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -46,21 +46,23 @@ import { checkOptInCode, activationLink, printTimeDuration } from './UserResolve import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' -import { getUserCreation } from './util/getUserCreation' -import { isContributionValid } from './util/isContributionValid' +import { + getCreationIndex, + getUserCreation, + getUserCreations, + isContributionValid, + isStartEndDateValid, +} from './util/isContributionValid' +import { + CONTRIBUTIONLINK_MEMO_MAX_CHARS, + CONTRIBUTIONLINK_MEMO_MIN_CHARS, + CONTRIBUTIONLINK_NAME_MAX_CHARS, + CONTRIBUTIONLINK_NAME_MIN_CHARS, + FULL_CREATION_AVAILABLE, +} from './const/const' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? -const MAX_CREATION_AMOUNT = new Decimal(1000) -export const FULL_CREATION_AVAILABLE = [ - MAX_CREATION_AMOUNT, - MAX_CREATION_AMOUNT, - MAX_CREATION_AMOUNT, -] -const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100 -const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5 -const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255 -const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5 @Resolver() export class AdminResolver { @@ -250,18 +252,17 @@ export class AdminResolver { const creations = await getUserCreation(user.id) logger.trace('creations', creations) const creationDateObj = new Date(creationDate) - if (isContributionValid(creations, amount, creationDateObj)) { - const contribution = Contribution.create() - contribution.userId = user.id - contribution.amount = amount - contribution.createdAt = new Date() - contribution.contributionDate = creationDateObj - contribution.memo = memo - contribution.moderatorId = moderator.id + isContributionValid(creations, amount, creationDateObj) + const contribution = Contribution.create() + contribution.userId = user.id + contribution.amount = amount + contribution.createdAt = new Date() + contribution.contributionDate = creationDateObj + contribution.memo = memo + contribution.moderatorId = moderator.id - logger.trace('contribution to save', contribution) - await Contribution.save(contribution) - } + logger.trace('contribution to save', contribution) + await Contribution.save(contribution) return getUserCreation(user.id) } @@ -404,9 +405,7 @@ export class AdminResolver { if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.') const creations = await getUserCreation(contribution.userId, false) - if (!isContributionValid(creations, contribution.amount, contribution.contributionDate)) { - throw new Error('Creation is not valid!!') - } + isContributionValid(creations, contribution.amount, contribution.contributionDate) const receivedCallDate = new Date() @@ -690,61 +689,6 @@ export class AdminResolver { } } -interface CreationMap { - id: number - creations: Decimal[] -} - -export async function getUserCreations( - ids: number[], - includePending = true, -): Promise { - logger.trace('getUserCreations:', ids, includePending) - const months = getCreationMonths() - logger.trace('getUserCreations months', months) - - const queryRunner = getConnection().createQueryRunner() - await queryRunner.connect() - - 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 - `) - - 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, - ) - return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) - }), - } - }) -} - function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] { const index = getCreationIndex(contribution.contributionDate.getMonth()) @@ -754,37 +698,3 @@ function updateCreations(creations: Decimal[], contribution: Contribution): Deci creations[index] = creations[index].plus(contribution.amount.toString()) return creations } - -const isStartEndDateValid = ( - startDate: string | null | undefined, - endDate: string | null | undefined, -): void => { - if (!startDate) { - logger.error('Start-Date is not initialized. A Start-Date must be set!') - throw new Error('Start-Date is not initialized. A Start-Date must be set!') - } - - if (!endDate) { - logger.error('End-Date is not initialized. An End-Date must be set!') - throw new Error('End-Date is not initialized. An End-Date must be set!') - } - - // check if endDate is before startDate - if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) { - logger.error(`The value of validFrom must before or equals the validTo!`) - throw new Error(`The value of validFrom must before or equals the validTo!`) - } -} - -const getCreationMonths = (): number[] => { - const now = new Date(Date.now()) - return [ - now.getMonth() + 1, - new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1, - new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1, - ].reverse() -} - -export const getCreationIndex = (month: number): number => { - return getCreationMonths().findIndex((el) => el === month + 1) -} diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index b083305e9..9d1dfc219 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -4,8 +4,7 @@ import { backendLogger as logger } from '@/server/logger' import { Contribution } from '@entity/Contribution' import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' import ContributionArgs from '../arg/ContributionArgs' -import { getUserCreation } from './util/getUserCreation' -import { isContributionValid } from './util/isContributionValid' +import { isContributionValid, getUserCreation } from './util/isContributionValid' @Resolver() export class ContributionResolver { diff --git a/backend/src/graphql/resolver/const/const.ts b/backend/src/graphql/resolver/const/const.ts new file mode 100644 index 000000000..d5ba08784 --- /dev/null +++ b/backend/src/graphql/resolver/const/const.ts @@ -0,0 +1,12 @@ +import Decimal from 'decimal.js-light' + +export const MAX_CREATION_AMOUNT = new Decimal(1000) +export const FULL_CREATION_AVAILABLE = [ + MAX_CREATION_AMOUNT, + MAX_CREATION_AMOUNT, + MAX_CREATION_AMOUNT, +] +export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100 +export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5 +export const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255 +export const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5 diff --git a/backend/src/graphql/resolver/util/getUserCreation.ts b/backend/src/graphql/resolver/util/getUserCreation.ts deleted file mode 100644 index 445e25b9c..000000000 --- a/backend/src/graphql/resolver/util/getUserCreation.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { getUserCreations, FULL_CREATION_AVAILABLE } from '../AdminResolver' - -export const getUserCreation = async (id: number, includePending = true): Promise => { - logger.trace('getUserCreation', id, includePending) - const creations = await getUserCreations([id], includePending) - return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE -} diff --git a/backend/src/graphql/resolver/util/isContributionValid.ts b/backend/src/graphql/resolver/util/isContributionValid.ts index 5ae7755c1..9dd59e6c9 100644 --- a/backend/src/graphql/resolver/util/isContributionValid.ts +++ b/backend/src/graphql/resolver/util/isContributionValid.ts @@ -1,12 +1,19 @@ +import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { backendLogger as logger } from '@/server/logger' +import { getConnection } from '@dbTools/typeorm' import Decimal from 'decimal.js-light' -import { getCreationIndex } from '../AdminResolver' +import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const' + +interface CreationMap { + id: number + creations: Decimal[] +} export const isContributionValid = ( creations: Decimal[], amount: Decimal, creationDate: Date, -): boolean => { +): void => { logger.trace('isContributionValid', creations, amount, creationDate) const index = getCreationIndex(creationDate.getMonth()) @@ -19,6 +26,94 @@ export const isContributionValid = ( `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`, ) } - - return true +} + +export async function getUserCreations( + ids: number[], + includePending = true, +): Promise { + logger.trace('getUserCreations:', ids, includePending) + const months = getCreationMonths() + logger.trace('getUserCreations months', months) + + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + + 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 + `) + + 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, + ) + return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) + }), + } + }) +} + +export const getUserCreation = async (id: number, includePending = true): Promise => { + logger.trace('getUserCreation', id, includePending) + const creations = await getUserCreations([id], includePending) + return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE +} + +export const getCreationMonths = (): number[] => { + const now = new Date(Date.now()) + return [ + now.getMonth() + 1, + new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1, + new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1, + ].reverse() +} + +export const getCreationIndex = (month: number): number => { + return getCreationMonths().findIndex((el) => el === month + 1) +} + +export const isStartEndDateValid = ( + startDate: string | null | undefined, + endDate: string | null | undefined, +): void => { + if (!startDate) { + logger.error('Start-Date is not initialized. A Start-Date must be set!') + throw new Error('Start-Date is not initialized. A Start-Date must be set!') + } + + if (!endDate) { + logger.error('End-Date is not initialized. An End-Date must be set!') + throw new Error('End-Date is not initialized. An End-Date must be set!') + } + + // check if endDate is before startDate + if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) { + logger.error(`The value of validFrom must before or equals the validTo!`) + throw new Error(`The value of validFrom must before or equals the validTo!`) + } } From 001117527cdf590d295786784959965bb1bc25c1 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 10:52:36 +0200 Subject: [PATCH 14/23] Change import of getUserCreation in isContributionValid. --- .../src/graphql/resolver/TransactionLinkResolver.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 83fd6367f..8d1555f58 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -28,8 +28,7 @@ import { executeTransaction } from './TransactionResolver' import { Order } from '@enum/Order' import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { isContributionValid } from './util/isContributionValid' -import { getUserCreation } from './util/getUserCreation' +import { getUserCreation, isContributionValid } from './util/isContributionValid' import { Decay } from '@model/Decay' import Decimal from 'decimal.js-light' import { TransactionTypeId } from '@enum/TransactionTypeId' @@ -224,13 +223,7 @@ export class TransactionLinkResolver { const creations = await getUserCreation(user.id, false) logger.info('open creations', creations) - if (!isContributionValid(creations, contributionLink.amount, now)) { - logger.error( - 'Amount of Contribution link exceeds available amount for this month', - contributionLink.amount, - ) - throw new Error('Amount of Contribution link exceeds available amount') - } + isContributionValid(creations, contributionLink.amount, now) const contribution = new DbContribution() contribution.userId = user.id contribution.createdAt = now From 6a55a27f888dfe293340c4b9a7fe1598e93668b4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 11:06:25 +0200 Subject: [PATCH 15/23] Removed tests with deleted user and user with unchecked email, add describe block for input not valid as well as valid input. --- .../resolver/ContributionResolver.test.ts | 173 ++++++------------ 1 file changed, 58 insertions(+), 115 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 3e5407811..43a57247e 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -46,70 +46,6 @@ describe('ContributionResolver', () => { }) }) - describe('authenticated with deleted user', () => { - beforeAll(async () => { - user = await userFactory(testEnv, stephenHawking) - await query({ - query: login, - variables: { email: 'stephen@hawking.uk', password: 'Aa12345_' }, - }) - }) - - afterAll(async () => { - await cleanDB() - resetToken() - }) - - it('throws unauthorized error', async () => { - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: 'not-valid', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) - }) - }) - - describe('authenticated with user that has not checked his email', () => { - beforeAll(async () => { - user = await userFactory(testEnv, garrickOllivander) - await query({ - query: login, - variables: { email: 'garrick@ollivander.com', password: 'Aa12345_' }, - }) - }) - - afterAll(async () => { - await cleanDB() - resetToken() - }) - - it('throws unauthorized error', async () => { - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: 'not-valid', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], - }), - ) - }) - }) - describe('authenticated with valid user', () => { beforeAll(async () => { user = await userFactory(testEnv, bibiBloxberg) @@ -124,59 +60,66 @@ describe('ContributionResolver', () => { resetToken() }) - it('throws error when creationDate not-valid', async () => { - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: 'not-valid', - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No information for available creations for the given date')], - }), - ) + describe('input not valid', () => { + it('throws error when creationDate not-valid', async () => { + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError('No information for available creations for the given date'), + ], + }), + ) + }) + + it('throws error when creationDate 3 month behind', async () => { + const date = new Date() + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: date.setMonth(date.getMonth() - 3).toString(), + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError('No information for available creations for the given date'), + ], + }), + ) + }) }) - it('throws error when creationDate 3 month behind', async () => { - const date = new Date() - date.setMonth(date.getMonth() - 3) - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: date.toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No information for available creations for the given date')], - }), - ) - }) - - it('creates contribution', async () => { - await expect( - mutate({ - mutation: createContribution, - variables: { - amount: 100.0, - memo: 'Test env contribution', - creationDate: new Date().toString(), - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - createContribution: true, - }, - }), - ) + describe('valid input', () => { + it('creates contribution', async () => { + await expect( + mutate({ + mutation: createContribution, + variables: { + amount: 100.0, + memo: 'Test env contribution', + creationDate: new Date().toString(), + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + createContribution: true, + }, + }), + ) + }) }) }) }) From 55cf4ffc5a2805e2728e1fd556ebf7a546540414 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 11:09:06 +0200 Subject: [PATCH 16/23] Change method description to => notation. --- backend/src/graphql/resolver/util/isContributionValid.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/util/isContributionValid.ts b/backend/src/graphql/resolver/util/isContributionValid.ts index 9dd59e6c9..0a9b57170 100644 --- a/backend/src/graphql/resolver/util/isContributionValid.ts +++ b/backend/src/graphql/resolver/util/isContributionValid.ts @@ -28,10 +28,10 @@ export const isContributionValid = ( } } -export async function getUserCreations( +export const getUserCreations = async ( ids: number[], includePending = true, -): Promise { +): Promise => { logger.trace('getUserCreations:', ids, includePending) const months = getCreationMonths() logger.trace('getUserCreations months', months) From 329a6c550040f84554ed9bd4472a346e76092eea Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 11:21:03 +0200 Subject: [PATCH 17/23] Fix linting. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 43a57247e..7d1a1b8e9 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -1,14 +1,10 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { peterLustig } from '@/seeds/users/peter-lustig' import { createContribution } from '@/seeds/graphql/mutations' import { login } from '@/seeds/graphql/queries' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { GraphQLError } from 'graphql' import { User } from '@entity/User' -import { Contribution } from '@entity/Contribution' import { userFactory } from '@/seeds/factory/user' -import { garrickOllivander } from '@/seeds/users/garrick-ollivander' -import { stephenHawking } from '@/seeds/users/stephen-hawking' let mutate: any, query: any, con: any let testEnv: any @@ -27,7 +23,6 @@ afterAll(async () => { }) let user: User -let creation: Contribution | void describe('ContributionResolver', () => { describe('createContribution', () => { From 790b6fb906ddc283c9db1f21474d9627499e9f49 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 11:23:12 +0200 Subject: [PATCH 18/23] Remove unused variable. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 7d1a1b8e9..5b12d1942 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { createContribution } from '@/seeds/graphql/mutations' import { login } from '@/seeds/graphql/queries' @@ -22,8 +25,6 @@ afterAll(async () => { await con.close() }) -let user: User - describe('ContributionResolver', () => { describe('createContribution', () => { describe('unauthenticated', () => { @@ -43,7 +44,7 @@ describe('ContributionResolver', () => { describe('authenticated with valid user', () => { beforeAll(async () => { - user = await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, bibiBloxberg) await query({ query: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, From 6a9b4f7b9a4f877afb9456f5d3f542e2c15df73a Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 11:24:12 +0200 Subject: [PATCH 19/23] Remove unused import. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 5b12d1942..ac5280757 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -6,7 +6,6 @@ import { createContribution } from '@/seeds/graphql/mutations' import { login } from '@/seeds/graphql/queries' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { GraphQLError } from 'graphql' -import { User } from '@entity/User' import { userFactory } from '@/seeds/factory/user' let mutate: any, query: any, con: any From c7ea879e36dbb91e3afb427bdf0152952f59e163 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 11:56:18 +0200 Subject: [PATCH 20/23] Change return value of createContribution from Boolean to UnconfirmedContribution, add constructor to UnconfirmedContribution. --- .../src/graphql/model/UnconfirmedContribution.ts | 15 +++++++++++++++ .../src/graphql/resolver/ContributionResolver.ts | 7 ++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/model/UnconfirmedContribution.ts b/backend/src/graphql/model/UnconfirmedContribution.ts index db4c08ce4..fd434e6df 100644 --- a/backend/src/graphql/model/UnconfirmedContribution.ts +++ b/backend/src/graphql/model/UnconfirmedContribution.ts @@ -1,8 +1,23 @@ import { ObjectType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' +import { Contribution } from '@entity/Contribution' +import { User } from '@entity/User' +import { FULL_CREATION_AVAILABLE } from '../resolver/const/const' @ObjectType() export class UnconfirmedContribution { + constructor(contribution: Contribution, user: User, creations: Decimal[]) { + this.id = contribution.id + this.userId = contribution.userId + this.amount = contribution.amount + this.memo = contribution.memo + this.date = contribution.contributionDate + this.firstName = user ? user.firstName : '' + this.lastName = user ? user.lastName : '' + this.email = user ? user.email : '' + this.creation = creations + } + @Field(() => String) firstName: string diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 9d1dfc219..924108f87 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -4,16 +4,17 @@ import { backendLogger as logger } from '@/server/logger' import { Contribution } from '@entity/Contribution' import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' import ContributionArgs from '../arg/ContributionArgs' +import { UnconfirmedContribution } from '../model/UnconfirmedContribution' import { isContributionValid, getUserCreation } from './util/isContributionValid' @Resolver() export class ContributionResolver { @Authorized([RIGHTS.CREATE_CONTRIBUTION]) - @Mutation(() => Boolean) + @Mutation(() => UnconfirmedContribution) async createContribution( @Args() { amount, memo, creationDate }: ContributionArgs, @Ctx() context: Context, - ): Promise { + ): Promise { const user = getUser(context) const creations = await getUserCreation(user.id) logger.trace('creations', creations) @@ -29,6 +30,6 @@ export class ContributionResolver { logger.trace('contribution to save', contribution) await Contribution.save(contribution) - return true + return new UnconfirmedContribution(contribution, user, creations) } } From 70b22f3dbec7c6337150358f5264ae33387a2f98 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 12:00:35 +0200 Subject: [PATCH 21/23] Remove unused import of FULL_CREATION_AVAILABLE const. --- backend/src/graphql/model/UnconfirmedContribution.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/model/UnconfirmedContribution.ts b/backend/src/graphql/model/UnconfirmedContribution.ts index fd434e6df..1d697a971 100644 --- a/backend/src/graphql/model/UnconfirmedContribution.ts +++ b/backend/src/graphql/model/UnconfirmedContribution.ts @@ -2,7 +2,6 @@ import { ObjectType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' import { Contribution } from '@entity/Contribution' import { User } from '@entity/User' -import { FULL_CREATION_AVAILABLE } from '../resolver/const/const' @ObjectType() export class UnconfirmedContribution { From ba88599ca7a7df506b7a8956d141efcf8a32066a Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 29 Jun 2022 13:31:57 +0200 Subject: [PATCH 22/23] Change mutation to query amount and memo, check in test that amount and memo have the right values. --- backend/src/graphql/resolver/ContributionResolver.test.ts | 5 ++++- backend/src/seeds/graphql/mutations.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index ac5280757..01e9b123e 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -110,7 +110,10 @@ describe('ContributionResolver', () => { ).resolves.toEqual( expect.objectContaining({ data: { - createContribution: true, + createContribution: { + amount: '100', + memo: 'Test env contribution', + }, }, }), ) diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 5383398b1..4926f706f 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -233,6 +233,9 @@ export const deleteContributionLink = gql` export const createContribution = gql` mutation ($amount: Decimal!, $memo: String!, $creationDate: String!) { - createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) + createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) { + amount + memo + } } ` From 103928518c2dceb0ddd2f643a6c4270a83365d98 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 5 Jul 2022 11:35:50 +0200 Subject: [PATCH 23/23] Change utils file isContributionValid to contributions and changed method isContributionValid to validateContribution. --- backend/src/graphql/resolver/AdminResolver.ts | 10 +++++----- backend/src/graphql/resolver/ContributionResolver.ts | 4 ++-- .../src/graphql/resolver/TransactionLinkResolver.ts | 4 ++-- .../util/{isContributionValid.ts => creations.ts} | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) rename backend/src/graphql/resolver/util/{isContributionValid.ts => creations.ts} (99%) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 5476fd8a1..75175edc2 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -50,9 +50,9 @@ import { getCreationIndex, getUserCreation, getUserCreations, - isContributionValid, + validateContribution, isStartEndDateValid, -} from './util/isContributionValid' +} from './util/creations' import { CONTRIBUTIONLINK_MEMO_MAX_CHARS, CONTRIBUTIONLINK_MEMO_MIN_CHARS, @@ -252,7 +252,7 @@ export class AdminResolver { const creations = await getUserCreation(user.id) logger.trace('creations', creations) const creationDateObj = new Date(creationDate) - isContributionValid(creations, amount, creationDateObj) + validateContribution(creations, amount, creationDateObj) const contribution = Contribution.create() contribution.userId = user.id contribution.amount = amount @@ -328,7 +328,7 @@ export class AdminResolver { } // all possible cases not to be true are thrown in this function - isContributionValid(creations, amount, creationDateObj) + validateContribution(creations, amount, creationDateObj) contributionToUpdate.amount = amount contributionToUpdate.memo = memo contributionToUpdate.contributionDate = new Date(creationDate) @@ -405,7 +405,7 @@ export class AdminResolver { if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.') const creations = await getUserCreation(contribution.userId, false) - isContributionValid(creations, contribution.amount, contribution.contributionDate) + validateContribution(creations, contribution.amount, contribution.contributionDate) const receivedCallDate = new Date() diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 924108f87..98492b510 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -5,7 +5,7 @@ import { Contribution } from '@entity/Contribution' import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql' import ContributionArgs from '../arg/ContributionArgs' import { UnconfirmedContribution } from '../model/UnconfirmedContribution' -import { isContributionValid, getUserCreation } from './util/isContributionValid' +import { validateContribution, getUserCreation } from './util/creations' @Resolver() export class ContributionResolver { @@ -19,7 +19,7 @@ export class ContributionResolver { const creations = await getUserCreation(user.id) logger.trace('creations', creations) const creationDateObj = new Date(creationDate) - isContributionValid(creations, amount, creationDateObj) + validateContribution(creations, amount, creationDateObj) const contribution = Contribution.create() contribution.userId = user.id diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 8d1555f58..8696065ed 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -28,7 +28,7 @@ import { executeTransaction } from './TransactionResolver' import { Order } from '@enum/Order' import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { getUserCreation, isContributionValid } from './util/isContributionValid' +import { getUserCreation, validateContribution } from './util/creations' import { Decay } from '@model/Decay' import Decimal from 'decimal.js-light' import { TransactionTypeId } from '@enum/TransactionTypeId' @@ -223,7 +223,7 @@ export class TransactionLinkResolver { const creations = await getUserCreation(user.id, false) logger.info('open creations', creations) - isContributionValid(creations, contributionLink.amount, now) + validateContribution(creations, contributionLink.amount, now) const contribution = new DbContribution() contribution.userId = user.id contribution.createdAt = now diff --git a/backend/src/graphql/resolver/util/isContributionValid.ts b/backend/src/graphql/resolver/util/creations.ts similarity index 99% rename from backend/src/graphql/resolver/util/isContributionValid.ts rename to backend/src/graphql/resolver/util/creations.ts index 0a9b57170..dcdce2bfa 100644 --- a/backend/src/graphql/resolver/util/isContributionValid.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -9,7 +9,7 @@ interface CreationMap { creations: Decimal[] } -export const isContributionValid = ( +export const validateContribution = ( creations: Decimal[], amount: Decimal, creationDate: Date,