diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index c10fc96de..1d0fd2856 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -26,6 +26,8 @@ export enum RIGHTS { LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS', GDT_BALANCE = 'GDT_BALANCE', CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION', + LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS', + UPDATE_CONTRIBUTION = 'UPDATE_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 2d9ac2deb..75e552e79 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -24,6 +24,8 @@ export const ROLE_USER = new Role('user', [ RIGHTS.LIST_TRANSACTION_LINKS, RIGHTS.GDT_BALANCE, RIGHTS.CREATE_CONTRIBUTION, + RIGHTS.LIST_CONTRIBUTIONS, + RIGHTS.UPDATE_CONTRIBUTION, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts new file mode 100644 index 000000000..dc1dd39e9 --- /dev/null +++ b/backend/src/graphql/model/Contribution.ts @@ -0,0 +1,43 @@ +import { ObjectType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' +import { Contribution as dbContribution } from '@entity/Contribution' +import { User } from './User' + +@ObjectType() +export class Contribution { + constructor(contribution: dbContribution, user: User) { + this.id = contribution.id + this.user = user + this.amount = contribution.amount + this.memo = contribution.memo + this.createdAt = contribution.createdAt + this.deletedAt = contribution.deletedAt + } + + @Field(() => Number) + id: number + + @Field(() => User) + user: User + + @Field(() => Decimal) + amount: Decimal + + @Field(() => String) + memo: string + + @Field(() => Date) + createdAt: Date + + @Field(() => Date, { nullable: true }) + deletedAt: Date | null +} + +@ObjectType() +export class ContributionListResult { + @Field(() => Int) + linkCount: number + + @Field(() => [Contribution]) + linkList: Contribution[] +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 5476fd8a1..962c13e9b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -52,6 +52,7 @@ import { getUserCreations, isContributionValid, isStartEndDateValid, + updateCreations, } from './util/isContributionValid' import { CONTRIBUTIONLINK_MEMO_MAX_CHARS, @@ -688,13 +689,3 @@ export class AdminResolver { return new ContributionLink(dbContributionLink) } } - -function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] { - const index = getCreationIndex(contribution.contributionDate.getMonth()) - - if (index < 0) { - throw new Error('You cannot create GDD for a month older than the last three months.') - } - creations[index] = creations[index].plus(contribution.amount.toString()) - return creations -} diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 01e9b123e..c9302cf64 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -2,11 +2,14 @@ /* 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' +import { createContribution, updateContribution } from '@/seeds/graphql/mutations' +import { listContributions, login } from '@/seeds/graphql/queries' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { GraphQLError } from 'graphql' import { userFactory } from '@/seeds/factory/user' +import { creationFactory } from '@/seeds/factory/creation' +import { creations } from '@/seeds/creation/index' +import { peterLustig } from '@/seeds/users/peter-lustig' let mutate: any, query: any, con: any let testEnv: any @@ -121,4 +124,114 @@ describe('ContributionResolver', () => { }) }) }) + + describe('listContributions', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, peterLustig) + await userFactory(testEnv, bibiBloxberg) + // bibi needs GDDs + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + // await userFactory(testEnv, bibiBloxberg) + await query({ + query: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(async () => { + await cleanDB() + resetToken() + }) + + it('returns an empty array for unconfirmed creation filter', async () => { + await expect( + query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: true, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listContributions: [], + }, + }), + ) + }) + + it('returns confirmed creation', async () => { + await expect( + query({ + query: listContributions, + variables: { + currentPage: 1, + pageSize: 25, + order: 'DESC', + filterConfirmed: false, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + listContributions: expect.arrayContaining([ + expect.objectContaining({ + memo: 'Herzlich Willkommen bei Gradido!', + amount: '1000', + }), + ]), + }, + }), + ) + }) + }) + }) + + describe('updateContribution', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + await expect( + mutate({ + mutation: updateContribution, + variables: { + contributionId: 1, + amount: 100.0, + memo: 'Test Contribution', + creationDate: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 924108f87..2399593c2 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -1,11 +1,16 @@ 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 { Contribution as dbContribution } from '@entity/Contribution' +import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' +import { IsNull } from '../../../../database/node_modules/typeorm' import ContributionArgs from '../arg/ContributionArgs' +import Paginated from '../arg/Paginated' +import { Order } from '../enum/Order' +import { Contribution } from '../model/Contribution' import { UnconfirmedContribution } from '../model/UnconfirmedContribution' -import { isContributionValid, getUserCreation } from './util/isContributionValid' +import { User } from '../model/User' +import { isContributionValid, getUserCreation, updateCreations } from './util/isContributionValid' @Resolver() export class ContributionResolver { @@ -21,7 +26,7 @@ export class ContributionResolver { const creationDateObj = new Date(creationDate) isContributionValid(creations, amount, creationDateObj) - const contribution = Contribution.create() + const contribution = dbContribution.create() contribution.userId = user.id contribution.amount = amount contribution.createdAt = new Date() @@ -29,7 +34,82 @@ export class ContributionResolver { contribution.memo = memo logger.trace('contribution to save', contribution) - await Contribution.save(contribution) + await dbContribution.save(contribution) return new UnconfirmedContribution(contribution, user, creations) } + + @Authorized([RIGHTS.LIST_CONTRIBUTIONS]) + @Query(() => [Contribution]) + async listContributions( + @Args() + { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, + @Arg('filterConfirmed', () => Boolean) + filterConfirmed: boolean | null, + @Ctx() context: Context, + ): Promise { + const user = getUser(context) + let contribution + if (filterConfirmed) { + contribution = await dbContribution.find({ + where: { + userId: user.id, + confirmedBy: IsNull(), + }, + order: { + createdAt: order, + }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + }) + } else { + contribution = await dbContribution.find({ + where: { + userId: user.id, + }, + order: { + createdAt: order, + }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + }) + } + return contribution.map((contr) => new Contribution(contr, new User(user))) + } + + @Authorized([RIGHTS.UPDATE_CONTRIBUTION]) + @Mutation(() => UnconfirmedContribution) + async updateContribution( + @Arg('contributionId', () => Int) + contributionId: number, + @Args() { amount, memo, creationDate }: ContributionArgs, + @Ctx() context: Context, + ): Promise { + const user = getUser(context) + + const contributionToUpdate = await dbContribution.findOne({ + where: { id: contributionId, confirmedAt: IsNull() }, + }) + if (!contributionToUpdate) { + throw new Error('No contribution found to given id.') + } + if (contributionToUpdate.userId !== user.id) { + throw new Error('user of the pending contribution and send user does not correspond') + } + + const creationDateObj = new Date(creationDate) + let creations = await getUserCreation(user.id) + if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { + creations = updateCreations(creations, contributionToUpdate) + } + + // all possible cases not to be true are thrown in this function + isContributionValid(creations, amount, creationDateObj) + contributionToUpdate.amount = amount + contributionToUpdate.memo = memo + contributionToUpdate.contributionDate = new Date(creationDate) + dbContribution.save(contributionToUpdate) + + const result = new UnconfirmedContribution(contributionToUpdate, user, creations) + return result + } } diff --git a/backend/src/graphql/resolver/util/isContributionValid.ts b/backend/src/graphql/resolver/util/isContributionValid.ts index 0a9b57170..552370f4c 100644 --- a/backend/src/graphql/resolver/util/isContributionValid.ts +++ b/backend/src/graphql/resolver/util/isContributionValid.ts @@ -1,6 +1,7 @@ import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { backendLogger as logger } from '@/server/logger' import { getConnection } from '@dbTools/typeorm' +import { Contribution } from '@entity/Contribution' import Decimal from 'decimal.js-light' import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const' @@ -117,3 +118,13 @@ export const isStartEndDateValid = ( throw new Error(`The value of validFrom must before or equals the validTo!`) } } + +export const updateCreations = (creations: Decimal[], contribution: Contribution): Decimal[] => { + const index = getCreationIndex(contribution.contributionDate.getMonth()) + + if (index < 0) { + throw new Error('You cannot create GDD for a month older than the last three months.') + } + creations[index] = creations[index].plus(contribution.amount.toString()) + return creations +} diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 4926f706f..45adeacc8 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -239,3 +239,17 @@ export const createContribution = gql` } } ` + +export const updateContribution = gql` + mutation ($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) { + updateContribution( + contributionId: $contributionId + amount: $amount + memo: $memo + creationDate: $creationDate + ) { + amount + memo + } + } +` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 531aebf0f..cf0fd09bd 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -172,6 +172,24 @@ export const queryTransactionLink = gql` } ` +export const listContributions = gql` + query ( + $currentPage: Int = 1 + $pageSize: Int = 5 + $order: Order + $filterConfirmed: Boolean = false + ) { + listContributions( + currentPage: $currentPage + pageSize: $pageSize + order: $order + filterConfirmed: $filterConfirmed + ) { + amount + memo + } + } +` // from admin interface export const listUnconfirmedContributions = gql`